[fix] Fix MassCalculator Unittests.

(Effectively a re-write of the MassCalculation code)
- renamed some mass calculator accesor methods
- MassData, InertiaMatrix refactored into 'RigidBody' class

- refactors out cache code to separate wrapper class
- calculations now use the Transform class to translate masses, CM, and MOI

- new class: MassCalculation
  - contains relevant context for a given calculation: tree-root, type, time, config
  - contains most actual calculation code
  - calculations are tracked with a context class: MassCalculation
This commit is contained in:
Daniel_M_Williams 2017-12-09 15:22:04 -05:00
parent 6289aef0ef
commit 20eff575f4
34 changed files with 1414 additions and 1315 deletions

View File

@ -96,7 +96,7 @@ public class BodyTubeDTO extends BasePartDTO implements AttachableParts {
final InnerTube innerTube = (InnerTube) rocketComponents;
final InnerBodyTubeDTO innerBodyTubeDTO = new InnerBodyTubeDTO(innerTube, this);
//Only add the inner tube if it is NOT a cluster.
if (innerTube.getClusterCount() == 1) {
if (innerTube.getInstanceCount() == 1) {
attachedParts.add(innerBodyTubeDTO);
}
} else if (rocketComponents instanceof BodyTube) {

View File

@ -62,7 +62,7 @@ public class InnerBodyTubeDTO extends BodyTubeDTO implements AttachableParts {
//Only if the inner tube is NOT a cluster, then create the corresponding Rocksim DTO and add it
//to the list of attached parts. If it is a cluster, then it is handled specially outside of this
//loop.
if (innerTube.getClusterCount() == 1) {
if (innerTube.getInstanceCount() == 1) {
attachedParts.add(new InnerBodyTubeDTO(innerTube, this));
}
} else if (rocketComponents instanceof BodyTube) {

View File

@ -17,6 +17,7 @@ import net.sf.openrocket.document.StorageOptions;
import net.sf.openrocket.file.RocketSaver;
import net.sf.openrocket.file.rocksim.RocksimCommonConstants;
import net.sf.openrocket.masscalc.MassCalculator;
import net.sf.openrocket.masscalc.RigidBody;
import net.sf.openrocket.rocketcomponent.AxialStage;
import net.sf.openrocket.rocketcomponent.FlightConfiguration;
import net.sf.openrocket.rocketcomponent.Rocket;
@ -92,11 +93,9 @@ public class RocksimSaver extends RocketSaver {
private RocketDesignDTO toRocketDesignDTO(Rocket rocket) {
RocketDesignDTO result = new RocketDesignDTO();
MassCalculator massCalc = new MassCalculator();
final FlightConfiguration configuration = rocket.getEmptyConfiguration();
final double cg = massCalc.getRocketSpentMassData(configuration).getCM().x *
RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
final RigidBody spentData = MassCalculator.calculateStructure( configuration);
final double cg = spentData.cm.x *RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
int stageCount = rocket.getStageCount();
if (stageCount == 3) {

View File

@ -0,0 +1,369 @@
package net.sf.openrocket.masscalc;
import java.util.ArrayList;
import net.sf.openrocket.motor.Motor;
import net.sf.openrocket.motor.MotorConfiguration;
import net.sf.openrocket.rocketcomponent.FlightConfiguration;
import net.sf.openrocket.rocketcomponent.MotorMount;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.util.Coordinate;
import net.sf.openrocket.util.MathUtil;
import net.sf.openrocket.util.Transformation;
/**
*
* @author teyrana (aka Daniel Williams) <equipoise@gmail.com>
*
*/
public class MassCalculation {
/**
* NOTE: Multiple enums may map to the same settings. This allows a caller to use the
* enum which best matches their use case.
*/
public enum Type {
// no motor data, even if the configuration contains active engines
STRUCTURE( true, false, false ),
// n.b. 'motor-mass' calculations are to be preferred over 'propellant-mass' calculations because they are computationally simpler:
// not only are they faster, but *slightly* more accurate because of fewer calculation rounding errors
MOTOR( false, true, true ),
BURNOUT( true, true, false ),
LAUNCH( true, true, true );
public final boolean includesStructure;
public final boolean includesMotorCasing;
public final boolean includesPropellant;
Type( double simulationTime ) {
includesStructure = false;
includesMotorCasing = true;
includesPropellant = true;
}
Type( boolean include_structure, boolean include_casing, boolean include_prop) {
this.includesStructure = include_structure;
this.includesMotorCasing = include_casing;
this.includesPropellant = include_prop;
}
}
// =========== Instance Functions ========================
public void merge( final MassCalculation other ) {
if( 0 < other.getMass()) {
// Adjust Center-of-mass
this.addMass( other.getCM() );
this.bodies.addAll( other.bodies );
}
}
public void addInertia( final RigidBody data ) {
this.bodies.add( data );
}
public void addMass( final Coordinate pointMass ) {
if( 0 == this.centerOfMass.weight ){
this.centerOfMass = pointMass;
}else {
this.centerOfMass = this.centerOfMass.average( pointMass);
}
}
public MassCalculation copy( final RocketComponent _root, final Transformation _transform ){
return new MassCalculation( this.type, this.config, this.simulationTime, _root, _transform );
}
public Coordinate getCM() {
return this.centerOfMass;
}
public double getMass() {
return this.centerOfMass.weight;
}
public double getLongitudinalInertia() {
return this.inertia.Iyy;
}
public double getRotationalInertia() {
return this.inertia.Ixx;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
MassCalculation other = (MassCalculation) obj;
return ((this.centerOfMass.equals(other.centerOfMass))
&& (this.config.equals( other.config))
&& (this.simulationTime == other.simulationTime)
&& (this.type == other.type) );
}
@Override
public int hashCode() {
return (int) (this.centerOfMass.hashCode());
}
public MassCalculation( final Type _type, final FlightConfiguration _config, final double _time, final RocketComponent _root, final Transformation _transform) {
type = _type;
config = _config;
simulationTime = _time;
root = _root;
transform = _transform;
reset();
}
public void setCM( final Coordinate newCM ) {
this.centerOfMass = newCM;
}
public void reset(){
centerOfMass = Coordinate.ZERO;
inertia = RigidBody.EMPTY;
bodies.clear();
}
public int size() {
return this.bodies.size();
}
public String toCMDebug(){
return String.format("cm= %.6fg@[%.6f,%.6f,%.6f]", centerOfMass.weight, centerOfMass.x, centerOfMass.y, centerOfMass.z);
}
// public String toMOIDebug(){
// return String.format("I_cm=[ %.8f, %.8f, %.8f ]", inertia.getIxx(), inertia.getIyy(), inertia.getIzz() ));
// }
@Override
public String toString() {
return this.toCMDebug();
}
// =========== Instance Member Variables ========================
private static final double MIN_MASS = MathUtil.EPSILON;
// === package-private ===
final FlightConfiguration config;
final double simulationTime;
final RocketComponent root;
final Transformation transform;
final Type type;
// center-of-mass only.
Coordinate centerOfMass = Coordinate.ZERO;
// center-of-mass AND moment-of-inertia data.
RigidBody inertia = RigidBody.EMPTY;
// center-of-mass AND moment-of-inertia data.
final ArrayList<RigidBody> bodies = new ArrayList<RigidBody>();
String prefix = "";
// =========== Private Instance Functions ========================
private MassCalculation calculateMountData(){
if( ! config.isComponentActive(this.root)) {
return this;
}
final MotorMount mount = (MotorMount)root;
MotorConfiguration motorConfig = mount.getMotorConfig( config.getId() );
final Motor motor = motorConfig.getMotor();
if( motorConfig.isEmpty() ){
return this;
}
final double mountXPosition = root.getOffset().x;
final int instanceCount = root.getInstanceCount();
final double motorXPosition = motorConfig.getX(); // location of motor from mount
final Coordinate[] offsets = root.getInstanceOffsets();
double eachMass;
double eachCMx; // CoM from beginning of motor
if ( this.type.includesMotorCasing && this.type.includesPropellant ){
eachMass = motor.getTotalMass( simulationTime );
eachCMx = motor.getCMx( simulationTime);
}else if( this.type.includesMotorCasing ) {
eachMass = motor.getTotalMass( Motor.PSEUDO_TIME_BURNOUT );
eachCMx = motor.getCMx( Motor.PSEUDO_TIME_BURNOUT );
} else {
final double eachMotorMass = motor.getTotalMass( simulationTime );
final double eachMotorCMx = motor.getCMx( simulationTime); // CoM from beginning of motor
final double eachCasingMass = motor.getBurnoutMass();
final double eachCasingCMx = motor.getBurnoutCGx();
eachMass = eachMotorMass - eachCasingMass;
eachCMx = (eachMotorCMx*eachMotorMass - eachCasingCMx*eachCasingMass)/eachMass;
}
// System.err.println(String.format("%-40s|Motor: %s.... Mass @%f = %.6f", prefix, motorConfig.toDescription(), simulationTime, eachMass ));
// coordinates in rocket frame; Ir, It about CoM.
final Coordinate clusterLocalCM = new Coordinate( mountXPosition + motorXPosition + eachCMx, 0, 0, eachMass*instanceCount);
double clusterBaseIr = motorConfig.getUnitRotationalInertia()*instanceCount*eachMass;
double clusterIt = motorConfig.getUnitLongitudinalInertia()*instanceCount*eachMass;
// if more than 1 moter => motors are not an the centerline => adjust via parallel-axis theorem
double clusterIr = clusterBaseIr;
if( 1 < instanceCount ){
for( Coordinate coord : offsets ){
double distance = Math.hypot( coord.y, coord.z);
clusterIr += eachMass*Math.pow( distance, 2);
}
}
final Coordinate clusterCM = transform.transform( clusterLocalCM );
addMass( clusterCM );
RigidBody clusterMOI = new RigidBody( clusterCM, clusterIr, clusterIt, clusterIt );
addInertia( clusterMOI );
return this;
}
/**
* Returns the mass and inertia data for this component and all subcomponents.
* The inertia is returned relative to the CG, and the CG is in the coordinates
* of the specified component, not global coordinates.
*
* @param calculation - i/o parameter to specifies the calculation parameters, and
* the instance returned with the calculation's tree data.
*
*/
/* package-scope */ MassCalculation calculateAssembly(){
final RocketComponent component = this.root;
final Transformation parentTransform = this.transform;
final int instanceCount = component.getInstanceCount();
Coordinate[] instanceLocations = component.getInstanceLocations();
// // vvv DEBUG
// if( this.config.isComponentActive(component) ){
// System.err.println(String.format( "%s[%s]....", prefix, component.getName()));
// }
if( this.type.includesStructure && this.config.isComponentActive(component) ){
Coordinate compCM = component.getCG();
double compIx = component.getRotationalUnitInertia() * compCM.weight;
double compIt = component.getLongitudinalUnitInertia() * compCM.weight;
if (!component.getOverrideSubcomponents()) {
if (component.isMassOverridden())
compCM = compCM.setWeight(MathUtil.max(component.getOverrideMass(), MIN_MASS));
if (component.isCGOverridden())
compCM = compCM.setXYZ(component.getOverrideCG());
}
// mass data for *this component only* in the rocket-frame
compCM = parentTransform.transform( compCM.add(component.getOffset()) );
this.addMass( compCM );
RigidBody componentInertia = new RigidBody( compCM, compIx, compIt, compIt );
this.addInertia( componentInertia );
// if( 0 < compCM.weight ) { // vvv DEBUG
// System.err.println(String.format( "%s....componentData: %s", prefix, compCM.toPreciseString() ));
// }
}
if( component.isMotorMount() && ( this.type.includesMotorCasing || this.type.includesPropellant )) {
MassCalculation propellant = this.copy(component, parentTransform);
propellant.calculateMountData();
this.merge( propellant );
// if( 0 < propellant.getMass() ) {
// System.err.println(String.format( "%s........++ propellantData: %s", prefix, propellant.toCMDebug()));
// }
}
// iterate over the aggregated instances for the whole tree.
MassCalculation children = this.copy(component, parentTransform );
for( int instanceNumber = 0; instanceNumber < instanceCount; ++instanceNumber) {
Coordinate currentLocation = instanceLocations[instanceNumber];
Transformation currentTransform = parentTransform.applyTransformation( Transformation.getTranslationTransform( currentLocation ));
for (RocketComponent child : component.getChildren()) {
// child data, relative to rocket reference frame
MassCalculation eachChild = copy( child, currentTransform);
eachChild.prefix = prefix + "....";
eachChild.calculateAssembly();
// accumulate children's data
children.merge( eachChild );
}
}
if( 0 < children.getMass() ) {
this.merge( children );
//System.err.println(String.format( "%s....assembly mass (incl/children): %s", prefix, this.toCMDebug()));
}
// Override total data
if (component.getOverrideSubcomponents()) {
if (component.isMassOverridden()) {
double newMass = MathUtil.max(component.getOverrideMass(), MIN_MASS);
Coordinate newCM = this.getCM().setWeight( newMass );
this.setCM( newCM );
}
if (component.isCGOverridden()) {
Coordinate newCM = this.getCM().setX( component.getOverrideCGX() );
this.setCM( newCM );
}
}
// vvv DEBUG
//if( this.config.isComponentActive(component) && 0 < this.getMass() ) {
//System.err.println(String.format( "%s....<< return assemblyData: %s (tree @%s)", prefix, this.toCMDebug(), component.getName() ));
// System.err.println(String.format( "%s Ixx = %.8f Iyy = %.8f", prefix, getIxx(), getIyy() ));
//}
// ^^^ DEBUG
return this;
}
/**
* MOI Calculation needs to be a two-step process:
* (1) calculate overall Center-of-Mass (CM) first (down inline with data-gathering)
* (2) Move MOIs to CM via parallel axis theorem (this method)
*
* @param Center-of-Mass where the MOI should be calculated around.
* @param inertias a list of component MassData instances to condense into a single MOI
*
* @return freshly calculated Moment-of-Inertia matrix
*/
/* package-scope */ RigidBody calculateMomentOfInertia() {
double Ir=0, It=0;
for( final RigidBody eachLocal : this.bodies ){
final RigidBody eachGlobal = eachLocal.rebase( this.centerOfMass );
Ir += eachGlobal.Ixx;
It += eachGlobal.Iyy;
}
return new RigidBody( centerOfMass, Ir, It, It );
}
}

View File

@ -1,241 +1,111 @@
package net.sf.openrocket.masscalc;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import net.sf.openrocket.masscalc.MassCalculation.Type;
import net.sf.openrocket.motor.Motor;
import net.sf.openrocket.motor.MotorConfiguration;
import net.sf.openrocket.rocketcomponent.AxialStage;
import net.sf.openrocket.rocketcomponent.FlightConfiguration;
import net.sf.openrocket.rocketcomponent.FlightConfigurationId;
import net.sf.openrocket.rocketcomponent.Instanceable;
import net.sf.openrocket.rocketcomponent.MotorMount;
import net.sf.openrocket.rocketcomponent.ParallelStage;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.simulation.MotorClusterState;
import net.sf.openrocket.simulation.SimulationStatus;
import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.Coordinate;
import net.sf.openrocket.util.MathUtil;
import net.sf.openrocket.util.Monitorable;
import net.sf.openrocket.util.Transformation;
public class MassCalculator implements Monitorable {
public static final double MIN_MASS = MathUtil.EPSILON;
//private static final Logger log = LoggerFactory.getLogger(MassCalculator.class);
public static final double MIN_MASS = 0.001 * MathUtil.EPSILON;
private int rocketMassModID = -1;
private int rocketTreeModID = -1;
private FlightConfigurationId configId = FlightConfigurationId.ERROR_FCID;
/*
* Cached data. All CG data is in absolute coordinates. All moments of inertia
* are relative to their respective CG.
*/
private HashMap< Integer, MassData> stageMassCache = new HashMap<Integer, MassData >();
private MassData rocketSpentMassCache;
private MassData propellantMassCache;
// private HashMap< Integer, MassData> stageMassCache = new HashMap<Integer, MassData >();
// private MassData rocketSpentMassCache;
// private MassData propellantMassCache;
private int modId=0;
////////////////// Constructors ///////////////////
public MassCalculator() {
}
////////////////// Mass property calculations ///////////////////
////////////////// Public Accessors ///////////////////
public MassData getRocketSpentMassData( final FlightConfiguration config ){
revalidateCache( config);
return this.rocketSpentMassCache;
}
public MassData getRocketLaunchMassData( final FlightConfiguration config ){
revalidateCache( config);
return rocketSpentMassCache.add( propellantMassCache );
/**
* Calculates mass data of the rocket's burnout mass
* - includes structure
* - includes motors
* - for Black Powder & Composite motors, this generally *excludes* propellant
*
* @param configuration the rocket configuration to calculate for
* @return the MassData struct of the motors at burnout
*/
public static RigidBody calculateStructure( final FlightConfiguration config) {
return calculate( MassCalculation.Type.STRUCTURE, config, Motor.PSEUDO_TIME_EMPTY );
}
/**
* Calculates mass data of the rocket's burnout mass
* - includes structure
* - includes motors
* - for Black Powder & Composite motors, this generally *excludes* propellant
*
* @param configuration the rocket configuration to calculate for
* @return the MassData struct of the motors at burnout
*/
public static RigidBody calculateBurnout( final FlightConfiguration config) {
return calculate( MassCalculation.Type.BURNOUT, config, Motor.PSEUDO_TIME_BURNOUT );
}
public static RigidBody calculateMotor( final FlightConfiguration config) {
return calculate( MassCalculation.Type.MOTOR, config, Motor.PSEUDO_TIME_LAUNCH );
}
/**
* Compute the burnout mass properties all motors, given a configuration
*
* Will calculate data for: MassCalcType.BURNOUT_MASS
*
* @param config the rocket configuration
* @return the MassData struct of the motors at burnout
*/
public static RigidBody calculateLaunch( final FlightConfiguration config ){
return calculate( MassCalculation.Type.LAUNCH, config, Motor.PSEUDO_TIME_LAUNCH );
}
/** calculates the massdata for all propellant in the rocket given the simulation status.
*
* @param status CurrentSimulation status to calculate data with
* @return combined mass data for all propellant
*/
public MassData getPropellantMassData( final SimulationStatus status ){
revalidateCache( status );
public static RigidBody calculateMotor( final SimulationStatus status ){
return calculate( MassCalculation.Type.MOTOR, status );
}
////////////////// Mass property Wrappers ///////////////////
// all mass calculation calls should probably call through one of these two wrappers.
// convenience wrapper -- use this to implicitly create a plain MassCalculation object with common parameters
public static RigidBody calculate( final MassCalculation.Type _type, final SimulationStatus status ){
final FlightConfiguration config = status.getConfiguration();
final double time = status.getSimulationTime();
MassCalculation calculation= new MassCalculation( _type, config, time, config.getRocket(), Transformation.IDENTITY);
return propellantMassCache;
calculation.calculateAssembly();
RigidBody result = calculation.calculateMomentOfInertia();
return result;
}
/** calculates the massdata @ launch for all propellant in the rocket
*
* @param status CurrentSimulation status to calculate data with
* @return combined mass data for all propellant
*/
protected MassData calculatePropellantMassData( final FlightConfiguration config ){
MassData allPropellantData = MassData.ZERO_DATA;
Collection<MotorConfiguration> activeMotorList = config.getActiveMotors();
for (MotorConfiguration mtrConfig : activeMotorList ) {
MassData curMotorConfigData = calculateClusterPropellantData( mtrConfig, Motor.PSEUDO_TIME_LAUNCH );
allPropellantData = allPropellantData.add( curMotorConfigData );
}
return allPropellantData;
}
/** calculates the massdata @ launch for all propellant in the rocket
*
* @param status CurrentSimulation status to calculate data with
* @return combined mass data for all propellant
*/
protected MassData calculatePropellantMassData( final SimulationStatus status ){
MassData allPropellantData = MassData.ZERO_DATA;
Collection<MotorClusterState> motorStates = status.getActiveMotors();
for (MotorClusterState state: motorStates) {
final double motorTime = state.getMotorTime( status.getSimulationTime() );
MassData clusterPropData = calculateClusterPropellantData( state.getConfig(), motorTime );
allPropellantData = allPropellantData.add( clusterPropData);
}
return allPropellantData;
}
// helper method to calculate the propellant mass data for a given motor cluster( i.e. MotorConfiguration)
private MassData calculateClusterPropellantData( final MotorConfiguration mtrConfig, final double motorTime ){
final Motor mtr = mtrConfig.getMotor();
final MotorMount mnt = mtrConfig.getMount();
final int instanceCount = mnt.getInstanceCount();
// location of mount, w/in entire rocket
final Coordinate[] locations = mnt.getLocations();
final double motorXPosition = mtrConfig.getX(); // location of motor from mount
final double propMassEach = mtr.getPropellantMass( motorTime );
final double propCMxEach = mtr.getCMx( motorTime); // CoM from beginning of motor
// coordinates in rocket frame; Ir, It about CoM.
final Coordinate curClusterCM = new Coordinate( locations[0].x + motorXPosition + propCMxEach, 0, 0, propMassEach*instanceCount);
final double unitRotationalInertiaEach = mtrConfig.getUnitRotationalInertia();
final double unitLongitudinalInertiaEach = mtrConfig.getUnitLongitudinalInertia();
double Ir=unitRotationalInertiaEach*instanceCount*propMassEach;
double It=unitLongitudinalInertiaEach*instanceCount*propMassEach;
if( 1 < instanceCount ){
// if not on rocket centerline, then add an offset factor, according to the parallel axis theorem:
for( Coordinate coord : locations ){
double distance = Math.hypot( coord.y, coord.z);
Ir += propMassEach*Math.pow( distance, 2);
}
}
return new MassData( curClusterCM, Ir, It);
}
/**
* Calculates mass data of the rocket burnout mass
*
* I.O.W., this mass data is invariant during thrust (see also: calculatePropellantMassData(...) )
*
* @param configuration a given rocket configuration
* @return the CG of the configuration
*/
protected MassData calculateBurnoutMassData( final FlightConfiguration config) {
MassData exceptMotorsMassData = calculateStageData( config);
MassData motorMassData = calculateMotorBurnoutMassData( config);
return exceptMotorsMassData.add( motorMassData );
// convenience wrapper -- use this to implicitly create a plain MassCalculation object with common parameters
public static RigidBody calculate( final MassCalculation.Type _type, final FlightConfiguration _config, double _time ){
MassCalculation calculation = new MassCalculation( _type, _config, _time, _config.getRocket(), Transformation.IDENTITY);
calculation.calculateAssembly();
return calculation.calculateMomentOfInertia();
}
private MassData calculateStageData( final FlightConfiguration config ){
MassData componentData = MassData.ZERO_DATA;
// Stages
for (AxialStage stage : config.getActiveStages()) {
int stageNumber = stage.getStageNumber();
MassData stageData = this.calculateAssemblyMassData( stage );
stageMassCache.put(stageNumber, stageData);
componentData = componentData.add(stageData);
}
return componentData;
}
/**
* Compute the burnout mass properties all motors, given a configuration
*
* Will calculate data for:MassCalcType.BURNOUT_MASS
*
* @param configuration the rocket configuration
* @return the MassData struct of the motors at burnout
*/
private MassData calculateMotorBurnoutMassData(FlightConfiguration config) {
MassData allMotorData = MassData.ZERO_DATA;
//int motorIndex = 0;
for (MotorConfiguration mtrConfig : config.getActiveMotors() ) {
Motor mtr = (Motor) mtrConfig.getMotor();
MotorMount mount = mtrConfig.getMount();
// use 'mount.getLocations()' because:
// 1) includes ALL clustering sources!
// 2) location of mount, w/in entire rocket
// 3) Note: mount.getInstanceCount() ONLY indicates instancing of the mount's cluster, not parent components (such as stages)
Coordinate[] locations = mount.getLocations();
int instanceCount = locations.length;
double motorXPosition = mtrConfig.getX(); // location of motor from mount
final double burnoutMassEach = mtr.getBurnoutMass();
final double burnoutCMx = mtr.getBurnoutCGx(); // CoM from beginning of motor
final Coordinate clusterCM = new Coordinate( locations[0].x + motorXPosition + burnoutCMx, 0, 0, burnoutMassEach*instanceCount);
final double unitRotationalInertia = mtrConfig.getUnitRotationalInertia();
final double unitLongitudinalInertia = mtrConfig.getUnitLongitudinalInertia();
double Ir=(unitRotationalInertia*burnoutMassEach)*instanceCount;
double It=(unitLongitudinalInertia*burnoutMassEach)*instanceCount;
if( 1 < instanceCount ){
for( Coordinate coord : locations ){
double distance_squared = ((coord.y*coord.y) + (coord.z*coord.z));
double instance_correction = burnoutMassEach*distance_squared;
Ir += instance_correction;
}
}
MassData configData = new MassData( clusterCM, Ir, It);
allMotorData = allMotorData.add( configData );
}
return allMotorData;
}
/**
* Return the total mass of the motors
*
* @param motors the motor configuration
* @param configuration the current motor instance configuration
* @return the total mass of all motors
*/
public double getPropellantMass(SimulationStatus status ){
return (getPropellantMassData( status )).getCM().weight;
}
/**
* Compute an analysis of the per-component CG's of the provided configuration.
@ -244,12 +114,22 @@ public class MassCalculator implements Monitorable {
* The CG of the entire configuration with motors is stored in the entry with the corresponding
* Rocket as the key.
*
* Deprecated:
* This function is fundamentally broken, because it asks for a calculation which ignores instancing.
* This function will work with simple rockets, but will be misleading or downright wrong for others.
*
* This is a problem with using a single-typed map:
* [1] multiple instances of components are not allowed, and must be merged.
* [2] propellant / motor data does not have a corresponding RocketComponent.
* ( or mount-data collides with motor-data )
*
* @param configuration the rocket configuration
* @param type the state of the motors (none, launch mass, burnout mass)
* @return a map from each rocket component to its corresponding CG.
*/
@Deprecated
public Map<RocketComponent, Coordinate> getCGAnalysis(FlightConfiguration configuration) {
revalidateCache(configuration);
// revalidateCache(configuration);
Map<RocketComponent, Coordinate> map = new HashMap<RocketComponent, Coordinate>();
@ -269,172 +149,16 @@ public class MassCalculator implements Monitorable {
return map;
}
/**
* Returns the mass and inertia data for this component and all subcomponents.
* The inertia is returned relative to the CG, and the CG is in the coordinates
* of the specified component, not global coordinates.
*/
private MassData calculateAssemblyMassData(RocketComponent component) {
return calculateAssemblyMassData(component, "....");
}
private MassData calculateAssemblyMassData(RocketComponent component, String indent) {
Coordinate compCM = component.getComponentCG();
double compIx = component.getRotationalUnitInertia() * compCM.weight;
double compIt = component.getLongitudinalUnitInertia() * compCM.weight;
if( 0 > compCM.weight ){
throw new BugException(" computed a negative rotational inertia value.");
}
if( 0 > compIx ){
throw new BugException(" computed a negative rotational inertia value.");
}
if( 0 > compIt ){
throw new BugException(" computed a negative longitudinal inertia value.");
}
if (!component.getOverrideSubcomponents()) {
if (component.isMassOverridden())
compCM = compCM.setWeight(MathUtil.max(component.getOverrideMass(), MIN_MASS));
if (component.isCGOverridden())
compCM = compCM.setXYZ(component.getOverrideCG());
}
// default if not instanced (instance count == 1)
MassData assemblyData = new MassData( compCM, compIx, compIt);
MassData childrenData = MassData.ZERO_DATA;
// Combine data for subcomponents
for (RocketComponent child : component.getChildren()) {
if( child instanceof ParallelStage ){
// this stage will be tallied separately... skip.
continue;
}
// child data, relative to parent's reference frame
MassData childData = calculateAssemblyMassData(child, indent+"....");
childrenData = childrenData.add( childData );
}
// if instanced, adjust children's data too.
if( 1 == component.getInstanceCount() ){
assemblyData = assemblyData.add( childrenData );
}else if( 0 < component.getChildCount()){
final double curIxx = childrenData.getIxx(); // MOI about x-axis
final double curIyy = childrenData.getIyy(); // MOI about y axis
final double curIzz = childrenData.getIzz(); // MOI about z axis
Coordinate templateCM = assemblyData.cm;
MassData instAccumData = new MassData(); // accumulator for instance MassData
Coordinate[] instanceLocations = ((Instanceable) component).getInstanceOffsets();
for( Coordinate curOffset : instanceLocations ){
Coordinate instanceCM = curOffset.add(templateCM);
MassData instanceData = new MassData( instanceCM, curIxx, curIyy, curIzz);
// 3) Project the template data to the new CM
// and add to the total
instAccumData = instAccumData.add( instanceData);
}
////////////////// Mass property calculations ///////////////////
assemblyData = assemblyData.add( instAccumData );
}
// move to parent's reference point
assemblyData = assemblyData.move( component.getOffset() );
if( component instanceof ParallelStage ){
// hacky correction for the fact Booster Stages aren't direct subchildren to the rocket
assemblyData = assemblyData.move( component.getParent().getOffset() );
}
// Override total data
if (component.getOverrideSubcomponents()) {
if (component.isMassOverridden()) {
double oldMass = assemblyData.getMass();
double newMass = MathUtil.max(component.getOverrideMass(), MIN_MASS);
Coordinate newCM = assemblyData.getCM().setWeight(newMass);
double newIxx = assemblyData.getIxx() * newMass / oldMass;
double newIyy = assemblyData.getIyy() * newMass / oldMass;
double newIzz = assemblyData.getIzz() * newMass / oldMass;
assemblyData = new MassData( newCM, newIxx, newIyy, newIzz );
}
if (component.isCGOverridden()) {
double oldx = assemblyData.getCM().x;
double newx = component.getOverrideCGX();
Coordinate delta = new Coordinate(newx-oldx, 0, 0);
assemblyData = assemblyData.move( delta );
}
}
return assemblyData;
}
public void revalidateCache( final SimulationStatus status ){
rocketSpentMassCache = calculateBurnoutMassData( status.getConfiguration() );
propellantMassCache = calculatePropellantMassData( status);
}
public void revalidateCache( final FlightConfiguration config ){
checkCache( config);
if( null == rocketSpentMassCache ){
rocketSpentMassCache = calculateBurnoutMassData(config);
}
if( null == propellantMassCache ){
propellantMassCache = calculatePropellantMassData( config);
}
}
/**
* Check the current cache consistency. This method must be called by all
* methods that may use any cached data before any other operations are
* performed. If the rocket has changed since the previous call to
* <code>checkCache()</code>, then {@link #voidMassCache()} is called.
* <p>
* This method performs the checking based on the rocket's modification IDs,
* so that these method may be called from listeners of the rocket itself.
*
* @param configuration the configuration of the current call
*/
protected final boolean checkCache(FlightConfiguration configuration) {
if (rocketMassModID != configuration.getRocket().getMassModID() ||
rocketTreeModID != configuration.getRocket().getTreeModID() ||
configId != configuration.getId()) {
rocketMassModID = configuration.getRocket().getMassModID();
rocketTreeModID = configuration.getRocket().getTreeModID();
configId = configuration.getId();
voidMassCache();
return false;
}
return true;
}
/**
* Void cached mass data. This method is called whenever a change occurs in
* the rocket structure that affects the mass of the rocket and when a new
* Rocket is used. This method must be overridden to void any cached data
* necessary. The method must call <code>super.voidMassCache()</code> during
* its execution.
*/
protected void voidMassCache() {
this.stageMassCache.clear();
this.rocketSpentMassCache=null;
this.propellantMassCache=null;
}
@Override
public int getModID() {
return 0;
return this.modId;
}
}

View File

@ -1,269 +0,0 @@
package net.sf.openrocket.masscalc;
import static net.sf.openrocket.util.MathUtil.pow2;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.Coordinate;
import net.sf.openrocket.util.MathUtil;
/**
* An immutable value object containing the mass data of a component, assembly or entire rocket.
*
* @author Sampo Niskanen <sampo.niskanen@iki.fi>
* @author Daniel Williams <equipoise@gmail.com>
*/
public class MassData {
private static final double MIN_MASS = MathUtil.EPSILON;
/* ASSUME: a MassData instance implicitly describes a bodies w.r.t. a reference point
* a) the cm locates the body from the reference point
* b) each MOI is about the reference point.
*/
public final Coordinate cm;
// Moment of Inertias:
// x-axis: through the rocket's nose
// y,z axes: mutually perpendicular to each other and the x-axis. Because a rocket
// is (mostly) axisymmetric, y-axis and z-axis placement is arbitrary.
// MOI about the Center of Mass
private final InertiaMatrix I_cm;
// implements a simplified, diagonal MOI
private class InertiaMatrix {
public final double xx;
public final double yy;
public final double zz;
public InertiaMatrix( double Ixx, double Iyy, double Izz){
if(( 0 > Ixx)||( 0 > Iyy)||( 0 > Izz)){
throw new BugException(" attempted to initialize an InertiaMatrix with a negative inertia value.");
}
this.xx = Ixx;
this.yy = Iyy;
this.zz = Izz;
}
public InertiaMatrix add( InertiaMatrix other){
return new InertiaMatrix( this.xx + other.xx, this.yy + other.yy, this.zz + other.zz);
}
/**
* This function returns a <b>copy</b> of this MassData translated to a new location via
* a simplified model.
*
* Assuming rotations are independent, and occur perpendicular to the principal axes,
* The above can be simplified to produce a diagonal newMOI in
* the form of the parallel axis theorem:
* [ oldMOI + m*d^2, ...]
*
* For the full version of the equations, see:
* [1] https://en.wikipedia.org/wiki/Parallel_axis_theorem#Tensor_generalization
* [2] http://www.kwon3d.com/theory/moi/triten.html
*
*
* @param delta vector position from center of mass to desired reference location
*
* @return MassData the new MassData instance
*/
private InertiaMatrix translateInertia( final Coordinate delta, final double mass){
double x2 = pow2(delta.x);
double y2 = pow2(delta.y);
double z2 = pow2(delta.z);
// See: Parallel Axis Theorem in the function comments.
// I = I + m L^2; L = sqrt( y^2 + z^2);
// ergo: I = I + m (y^2 + z^2);
double newIxx = this.xx + mass*(y2 + z2);
double newIyy = this.yy + mass*(x2 + z2);
double newIzz = this.zz + mass*(x2 + y2);
// MOI about the reference point
InertiaMatrix toReturn=new InertiaMatrix( newIxx, newIyy, newIzz);
return toReturn;
}
@Override
public int hashCode() {
return (int) (Double.doubleToLongBits(this.xx) ^ Double.doubleToLongBits(this.yy) ^ Double.doubleToLongBits(this.xx) );
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!(obj instanceof InertiaMatrix))
return false;
InertiaMatrix other = (InertiaMatrix) obj;
return (MathUtil.equals(this.xx, other.xx) && MathUtil.equals(this.yy, other.yy) &&
MathUtil.equals(this.zz, other.zz)) ;
}
}
public static final MassData ZERO_DATA = new MassData(Coordinate.ZERO, 0, 0, 0);
public MassData() {
this.cm = Coordinate.ZERO;
this.I_cm = new InertiaMatrix(0,0,0);
}
/**
* Return a new instance of MassData which is a sum of this and the other.
*
* @param childData second mass data to combine with this
*
* @return MassData the new MassData instance
*/
public MassData add(MassData body2){
MassData body1 = this;
// the combined Center of mass is defined to be 'point 3'
Coordinate combinedCM = body1.cm.average( body2.cm );
// transform InertiaMatrix from it's previous frame to the common frame
Coordinate delta1 = combinedCM.sub( body1.cm).setWeight(0);
InertiaMatrix I1_at_3 = body1.I_cm.translateInertia(delta1, body1.getMass());
// transform InertiaMatrix from it's previous frame to the common frame
Coordinate delta2 = combinedCM.sub( body2.cm).setWeight(0);
InertiaMatrix I2_at_3 = body2.I_cm.translateInertia(delta2, body2.getMass());
// once both are in the same frame, simply add them
InertiaMatrix combinedMOI = I1_at_3.add(I2_at_3);
MassData sumData = new MassData( combinedCM, combinedMOI);
return sumData;
}
private MassData(Coordinate newCM, InertiaMatrix newMOI){
this.cm = newCM;
this.I_cm = newMOI;
}
public MassData( RocketComponent component ){
//MassData parentData = new MassData( parentCM, parentIx, parentIt);
// Calculate data for this component
Coordinate newCM = component.getComponentCG();
double mass = newCM.weight;
if ( mass < MIN_MASS){
newCM = newCM .setWeight(MIN_MASS);
mass = MIN_MASS;
}
this.cm = newCM;
double Ix = component.getRotationalUnitInertia() * mass;
double It = component.getLongitudinalUnitInertia() * mass;
this.I_cm = new InertiaMatrix( Ix, It, It);
}
public MassData(Coordinate newCM, double newIxx, double newIyy, double newIzz){
if (newCM == null) {
throw new IllegalArgumentException("CM is null");
}
this.cm = newCM;
this.I_cm = new InertiaMatrix(newIxx, newIyy, newIzz);
}
public MassData(final Coordinate cg, final double rotationalInertia, final double longitudinalInertia) {
if (cg == null) {
throw new IllegalArgumentException("cg is null");
}
this.cm = cg;
double newIxx = rotationalInertia;
double newIyy = longitudinalInertia;
double newIzz = longitudinalInertia;
this.I_cm = new InertiaMatrix( newIxx, newIyy, newIzz);
}
public double getMass(){
return cm.weight;
}
public Coordinate getCG() {
return cm;
}
public Coordinate getCM() {
return cm;
}
public double getIyy(){
return I_cm.yy;
}
public double getLongitudinalInertia() {
return I_cm.yy;
}
public double getIxx(){
return I_cm.xx;
}
public double getRotationalInertia() {
return I_cm.xx;
}
public double getIzz(){
return I_cm.zz;
}
/**
* Return a new instance of MassData moved by the delta vector supplied.
* This function is intended to move the REFERENCE POINT, not the CM, and will leave
* the Inertia matrix completely unchanged.
*
* ASSUME: MassData implicity describe their respective bodies w.r.t 0,0,0)
* a) the cm locates the body from the reference point
* b) each MOI is about the reference point.
*
* @param delta vector from current reference point to new/desired reference point (mass is ignored)
*
* @return MassData a new MassData instance, locating the same CM from a different reference point.
*/
public MassData move( final Coordinate delta ){
MassData body1 = this;
// don't change the mass, just move it.
Coordinate newCM = body1.cm.add( delta );
MassData newData = new MassData( newCM, this.I_cm);
return newData;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!(obj instanceof MassData))
return false;
MassData other = (MassData) obj;
return (this.cm.equals(other.cm) && (this.I_cm.equals(other.I_cm)));
}
@Override
public int hashCode() {
return (int) (cm.hashCode() ^ I_cm.hashCode() );
}
public String toCMDebug(){
return String.format("cm= %6.4fg@[%g,%g,%g]", cm.weight, cm.x, cm.y, cm.z);
}
public String toDebug(){
return toCMDebug()+" " +
String.format("I_cm=[ %g, %g, %g ]", I_cm.xx, I_cm.yy, I_cm.zz);
}
@Override
public String toString() {
return "MassData [cg=" + cm
+ ", rotationalInertia=" + getIxx() + ", longitudinalInertia=" + getIyy() + "]";
}
}

View File

@ -0,0 +1,141 @@
package net.sf.openrocket.masscalc;
import static net.sf.openrocket.util.MathUtil.pow2;
import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.Coordinate;
import net.sf.openrocket.util.MathUtil;
// implements a simplified, diagonal MOI
public class RigidBody {
public final Coordinate cm;
public final double Ixx;
public final double Iyy;
public final double Izz;
public static final RigidBody EMPTY = new RigidBody( Coordinate.ZERO, 0., 0., 0.);
public RigidBody( Coordinate _cm, double I_axial, double I_long ){
this( _cm, I_axial, I_long, I_long );
}
public RigidBody( Coordinate _cm, double Ixx, double Iyy, double Izz){
if(( 0 > Ixx)||( 0 > Iyy)||( 0 > Izz)){
throw new BugException(" attempted to initialize an InertiaMatrix with a negative inertia value.");
}
this.cm=_cm;
this.Ixx = Ixx;
this.Iyy = Iyy;
this.Izz = Izz;
}
public RigidBody add( RigidBody that){
final Coordinate newCM = this.cm.average( that.cm);
RigidBody movedThis = this.rebase( newCM );
RigidBody movedThat = that.rebase( newCM );
final double newIxx = movedThis.Ixx + movedThat.Ixx;
final double newIyy = movedThis.Iyy + movedThat.Iyy;
final double newIzz = movedThis.Izz + movedThat.Izz;
return new RigidBody( newCM, newIxx, newIyy, newIzz );
}
public Coordinate getCenterOfMass() { return cm; }
public Coordinate getCM() { return cm; }
public double getIyy(){ return Iyy; }
public double getIxx(){ return Ixx; }
public double getIzz(){ return Izz; }
public double getLongitudinalInertia() { return Iyy; }
public double getMass() { return this.cm.weight; }
public double getRotationalInertia() { return Ixx; }
public boolean isEmpty() {
if( RigidBody.EMPTY == this) {
return true;
}
return RigidBody.EMPTY.equals( this );
}
@Override
public int hashCode() {
return (int) (Double.doubleToLongBits(this.Ixx) ^ Double.doubleToLongBits(this.Iyy) ^ Double.doubleToLongBits(this.Ixx) );
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!(obj instanceof RigidBody))
return false;
RigidBody other = (RigidBody) obj;
return (MathUtil.equals(this.Ixx, other.Ixx) && MathUtil.equals(this.Iyy, other.Iyy) &&
MathUtil.equals(this.Izz, other.Izz)) ;
}
public RigidBody rebase( final Coordinate newLocation ){
final Coordinate delta = this.cm.sub( newLocation ).setWeight(0.);
double x2 = pow2(delta.x);
double y2 = pow2(delta.y);
double z2 = pow2(delta.z);
// final double radialDistanceSquared = (y2 + z2);
// final double axialDistanceSquared = x2;
//
// System.err.println(String.format(" - CM_new = %.8f @[ %.8f, %.8f, %.8f ]", eachGlobal.cm.weight, eachGlobal.cm.x, eachGlobal.cm.y, eachGlobal.cm.z ));
// System.err.println(String.format(" - each: base: { %.12f %.12f }", eachLocal.xx, eachLocal.xx ));
// System.err.println(String.format(" - each: carrected: { %.12f %.12f }", eachGlobal.xx, eachGlobal.yy ));
// See: Parallel Axis Theorem in the function comments.
// I = I + m L^2; L = sqrt( y^2 + z^2);
// ergo: I = I + m (y^2 + z^2);
double newIxx = this.Ixx + cm.weight*(y2 + z2);
double newIyy = this.Iyy + cm.weight*(x2 + z2);
double newIzz = this.Izz + cm.weight*(x2 + y2);
// MOI about the reference point
return new RigidBody( newLocation, newIxx, newIyy, newIzz);
}
@Override
public String toString() {
return toCMString()+" // "+toMOIString();
}
public String toCMString() {
return String.format("CoM: %.8fg @[%.8f,%.8f,%.8f]", cm.weight, cm.x, cm.y, cm.z);
}
public String toMOIString() {
return String.format("MOI: [ %.8f, %.8f, %.8f]", Ixx, Iyy, Izz );
}
/**
* This function returns a <b>copy</b> of this MassData translated to a new location via
* a simplified model.
*
* Assuming rotations are independent, and occur perpendicular to the principal axes,
* The above can be simplified to produce a diagonal newMOI in
* the form of the parallel axis theorem:
* [ oldMOI + m*d^2, ...]
*
* For the full version of the equations, see:
* [1] https://en.wikipedia.org/wiki/Parallel_axis_theorem#Tensor_generalization
* [2] http://www.kwon3d.com/theory/moi/triten.html
*
*
* @param delta vector position from center of mass to desired reference location
*
* @return MassData the new MassData instance
*/
public RigidBody translateInertia( final Coordinate delta ){
final Coordinate newLocation = this.cm.add( delta );
return rebase( newLocation );
}
}

View File

@ -62,9 +62,7 @@ public class StabilityDomain implements SimulationDomain {
* Caching would in any case be inefficient since the rocket changes all the time.
*/
AerodynamicCalculator aerodynamicCalculator = new BarrowmanCalculator();
MassCalculator massCalculator = new MassCalculator();
FlightConfiguration configuration = simulation.getRocket().getSelectedConfiguration();
FlightConditions conditions = new FlightConditions(configuration);
conditions.setMach(Application.getPreferences().getDefaultMach());
@ -73,7 +71,7 @@ public class StabilityDomain implements SimulationDomain {
// TODO: HIGH: This re-calculates the worst theta value every time
cp = aerodynamicCalculator.getWorstCP(configuration, conditions, null);
cg = massCalculator.getRocketLaunchMassData(configuration).getCM();
cg = MassCalculator.calculateLaunch( configuration).getCM();
if (cp.weight > 0.000001)
cpx = cp.x;

View File

@ -55,8 +55,6 @@ public class StabilityParameter implements OptimizableParameter {
* Caching would in any case be inefficient since the rocket changes all the time.
*/
AerodynamicCalculator aerodynamicCalculator = new BarrowmanCalculator();
MassCalculator massCalculator = new MassCalculator();
FlightConfiguration configuration = simulation.getRocket().getSelectedConfiguration();
FlightConditions conditions = new FlightConditions(configuration);
@ -66,7 +64,7 @@ public class StabilityParameter implements OptimizableParameter {
cp = aerodynamicCalculator.getWorstCP(configuration, conditions, null);
// worst case CM is also
cg = massCalculator.getRocketLaunchMassData(configuration).getCM();
cg = MassCalculator.calculateLaunch(configuration).getCM();
if (cp.weight > 0.000001)
cpx = cp.x;

View File

@ -3,10 +3,6 @@ package net.sf.openrocket.rocketcomponent;
import net.sf.openrocket.util.ChangeSource;
public interface Clusterable extends ChangeSource, Instanceable {
@Deprecated
// redundant with Instanceable#getInstanceCount()
public int getClusterCount();
public ClusterConfiguration getClusterConfiguration();

View File

@ -167,6 +167,10 @@ public class FlightConfiguration implements FlightConfigurableParameter<FlightCo
return stages.get(stageNumber).active;
}
// this method is deprecated because it ignores instancing of parent components (e.g. Strapons or pods )
// if you're calling this method, you're probably not getting the numbers you expect.
@Deprecated
public Collection<RocketComponent> getActiveComponents() {
Queue<RocketComponent> toProcess = new ArrayDeque<RocketComponent>(this.getActiveStages());
ArrayList<RocketComponent> toReturn = new ArrayList<>();

View File

@ -134,15 +134,6 @@ public class InnerTube extends ThicknessRingComponent implements Clusterable, Ra
}
}
/**
* Return the number of tubes in the cluster.
* @return Number of tubes in the current cluster.
*/
@Override
public int getClusterCount() {
return cluster.getClusterCount();
}
@Override
public int getInstanceCount() {
return cluster.getClusterCount();
@ -212,9 +203,8 @@ public class InnerTube extends ThicknessRingComponent implements Clusterable, Ra
return 2 * getOuterRadius() * clusterScale;
}
public List<Coordinate> getClusterPoints() {
List<Coordinate> list = new ArrayList<Coordinate>(getClusterCount());
List<Coordinate> list = new ArrayList<Coordinate>(getInstanceCount());
List<Double> points = cluster.getPoints(clusterRotation - getRadialDirection());
double separation = getClusterSeparation();
for (int i = 0; i < points.size() / 2; i++) {
@ -226,7 +216,7 @@ public class InnerTube extends ThicknessRingComponent implements Clusterable, Ra
@Override
public Coordinate[] getInstanceOffsets(){
if ( 1 == getClusterCount())
if ( 1 == getInstanceCount())
return new Coordinate[] { Coordinate.ZERO };
List<Coordinate> points = getClusterPoints();

View File

@ -173,18 +173,18 @@ public abstract class RingComponent extends StructuralComponent implements Coaxi
@Override
public Coordinate getComponentCG() {
Coordinate cg = Coordinate.ZERO;
int instanceCount = getInstanceCount();
double instanceMass = ringMass(getOuterRadius(), getInnerRadius(), getLength(),
getMaterial().getDensity());
final int instanceCount = getInstanceCount();
final double instanceMass = ringMass(getOuterRadius(), getInnerRadius(), getLength(), getMaterial().getDensity());
if (1 == instanceCount ) {
cg = new Coordinate( length/2, 0, 0, instanceMass );
}else{
Coordinate offsets[] = getInstanceOffsets();
for( Coordinate c : offsets) {
cg = cg.average(c);
c = c.setWeight( instanceMass );
cg = cg.average(c);
}
cg.add( length/2, 0, 0);
cg = cg.add( length/2, 0, 0);
}
return cg;
}

View File

@ -1220,7 +1220,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab
// == improperly initialized components OR the root Rocket instance
return getInstanceOffsets();
} else {
Coordinate[] parentPositions = this.parent.getLocations();
Coordinate[] parentPositions = this.parent.getComponentLocations();
int parentCount = parentPositions.length;
// override <instance>.getInstanceOffsets() in the subclass you want to fix.
@ -1264,7 +1264,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab
checkState();
final String lockText = "toAbsolute";
mutex.lock(lockText);
Coordinate[] thesePositions = this.getLocations();
Coordinate[] thesePositions = this.getComponentLocations();
final int instanceCount = thesePositions.length;
@ -2236,6 +2236,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab
buffer.append("\n ====== ====== ====== ====== ====== ====== ====== ====== ====== ====== ====== ======\n");
buffer.append(" [Name] [Length] [Rel Pos] [Abs Pos] \n");
this.toDebugTreeHelper(buffer, "");
buffer.append("\n ====== ====== ====== ====== ====== ====== ====== ====== ====== ====== ====== ======\n");
return buffer.toString();
}
@ -2251,27 +2252,13 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab
public void toDebugTreeNode(final StringBuilder buffer, final String indent) {
final String prefix = String.format("%s%s", indent, this.getName());
// 1) instanced vs non-instanced
if( 1 == getInstanceCount() ){
// un-instanced RocketComponents (usual case)
buffer.append(String.format("%-40s| %5.3f; %24s; %24s; ", prefix, this.getLength(), this.getOffset(), this.getLocations()[0]));
buffer.append(String.format("(offset: %4.1f via: %s )\n", this.getAxialOffset(), this.relativePosition.name()));
}else if( this instanceof Instanceable ){
// instanced components -- think motor clusters or booster stage clusters
final String patternName = ((Instanceable)this).getPatternName();
buffer.append(String.format("%-40s (cluster: %s )", prefix, patternName));
buffer.append(String.format("(offset: %4.1f via: %s )\n", this.getAxialOffset(), this.relativePosition.name()));
for (int instanceNumber = 0; instanceNumber < this.getInstanceCount(); instanceNumber++) {
final String instancePrefix = String.format("%s [%2d/%2d]", indent, instanceNumber+1, getInstanceCount());
buffer.append(String.format("%-40s| %5.3f; %24s; %24s;\n", instancePrefix, getLength(), getOffset(), getLocations()[0]));
}
}else{
throw new IllegalStateException("This is a developer error! If you implement an instanced class, please subclass the Clusterable interface.");
String prefix = String.format("%s%s", indent, this.getName());
if( 0 < this.getInstanceCount() ){
prefix = prefix + String.format(" (x%d)", this.getInstanceCount() );
}
buffer.append(String.format("%-40s| %6.4f; %24s; %24s; \n", prefix, getLength(), getOffset().toPreciseString(), getComponentLocations()[0].toPreciseString() ));
// 2) if this is an ACTING motor mount:
if(( this instanceof MotorMount ) &&( ((MotorMount)this).isMotorMount())){
// because InnerTube and BodyTube don't really share a common ancestor besides RocketComponent
@ -2284,10 +2271,10 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab
public void toDebugMountNode(final StringBuilder buffer, final String indent) {
MotorMount mnt = (MotorMount)this;
Coordinate[] relCoords = this.getInstanceOffsets();
Coordinate[] absCoords = this.getLocations();
// Coordinate[] relCoords = this.getInstanceOffsets();
Coordinate[] absCoords = this.getComponentLocations();
FlightConfigurationId curId = this.getRocket().getSelectedConfiguration().getFlightConfigurationID();
final int intanceCount = this.getInstanceCount();
// final int instanceCount = this.getInstanceCount();
MotorConfiguration curInstance = mnt.getMotorConfig( curId);
if( curInstance.isEmpty() ){
// print just the tube locations
@ -2296,6 +2283,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab
// curInstance has a motor ...
Motor curMotor = curInstance.getMotor();
final double motorOffset = this.getLength() - curMotor.getLength();
final String instancePrefix = String.format("%s [ */%2d]", indent, getInstanceCount());
buffer.append(String.format("%-40sThrust: %f N; \n",
indent+" Mounted: "+curMotor.getDesignation(), curMotor.getMaxThrustEstimate() ));
@ -2303,9 +2291,13 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab
Coordinate motorRelativePosition = new Coordinate(motorOffset, 0, 0);
Coordinate tubeAbs = absCoords[0];
Coordinate motorAbsolutePosition = new Coordinate(tubeAbs.x+motorOffset,tubeAbs.y,tubeAbs.z);
buffer.append(String.format("%-40s| %5.3f; %24s; %24s;\n", indent, curMotor.getLength(), motorRelativePosition, motorAbsolutePosition));
buffer.append(String.format("%-40s| %5.3f; %24s; %24s;\n", instancePrefix, curMotor.getLength(), motorRelativePosition, motorAbsolutePosition));
}
}
public boolean isMotorMount() {
return false;
}
}

View File

@ -3,7 +3,7 @@ package net.sf.openrocket.simulation;
import java.util.Collection;
import net.sf.openrocket.masscalc.MassCalculator;
import net.sf.openrocket.masscalc.MassData;
import net.sf.openrocket.masscalc.RigidBody;
import net.sf.openrocket.models.atmosphere.AtmosphericConditions;
import net.sf.openrocket.simulation.exception.SimulationException;
import net.sf.openrocket.simulation.listeners.SimulationListenerHelper;
@ -110,53 +110,47 @@ public abstract class AbstractSimulationStepper implements SimulationStepper {
* @return the mass data to use
* @throws SimulationException if a listener throws SimulationException
*/
protected MassData calculateDryMassData(SimulationStatus status) throws SimulationException {
MassData mass;
protected RigidBody calculateStructureMass(SimulationStatus status) throws SimulationException {
RigidBody structureMass;
// Call pre-listener
mass = SimulationListenerHelper.firePreMassCalculation(status);
if (mass != null) {
return mass;
structureMass = SimulationListenerHelper.firePreMassCalculation(status);
if (structureMass != null) {
return structureMass;
}
MassCalculator calc = status.getSimulationConditions().getMassCalculator();
mass = calc.getRocketSpentMassData( status.getConfiguration() );
structureMass = MassCalculator.calculateStructure( status.getConfiguration() );
// Call post-listener
mass = SimulationListenerHelper.firePostMassCalculation(status, mass);
structureMass = SimulationListenerHelper.firePostMassCalculation(status, structureMass);
checkNaN(mass.getCG());
checkNaN(mass.getLongitudinalInertia());
checkNaN(mass.getRotationalInertia());
checkNaN(structureMass.getCenterOfMass());
checkNaN(structureMass.getLongitudinalInertia());
checkNaN(structureMass.getRotationalInertia());
return mass;
return structureMass;
}
protected MassData calculatePropellantMassData(SimulationStatus status) throws SimulationException {
MassData mass;
protected RigidBody calculateMotorMass(SimulationStatus status) throws SimulationException {
RigidBody motorMass;
// Call pre-listener
mass = SimulationListenerHelper.firePreMassCalculation(status);
if (mass != null) {
return mass;
motorMass = SimulationListenerHelper.firePreMassCalculation(status);
if (motorMass != null) {
return motorMass;
}
MassCalculator calc = status.getSimulationConditions().getMassCalculator();
mass = calc.getPropellantMassData( status );
motorMass = MassCalculator.calculateMotor( status );
// Call post-listener
mass = SimulationListenerHelper.firePostMassCalculation(status, mass);
motorMass = SimulationListenerHelper.firePostMassCalculation(status, motorMass);
checkNaN(mass.getCG());
checkNaN(mass.getLongitudinalInertia());
checkNaN(mass.getRotationalInertia());
checkNaN(motorMass.getCenterOfMass());
checkNaN(motorMass.getLongitudinalInertia());
checkNaN(motorMass.getRotationalInertia());
return mass;
return motorMass;
}

View File

@ -1,6 +1,5 @@
package net.sf.openrocket.simulation;
import net.sf.openrocket.masscalc.MassData;
import net.sf.openrocket.models.atmosphere.AtmosphericConditions;
import net.sf.openrocket.rocketcomponent.RecoveryDevice;
import net.sf.openrocket.simulation.exception.SimulationException;
@ -39,8 +38,7 @@ public class BasicLandingStepper extends AbstractSimulationStepper {
// Compute drag force
double dynP = (0.5 * atmosphere.getDensity() * airSpeed.length2());
double dragForce = totalCD * dynP * refArea;
MassData massData = calculateDryMassData(status);
double mass = massData.getCG().weight;
double mass = calculateStructureMass(status).getMass();
// Compute drag acceleration

View File

@ -1,6 +1,6 @@
package net.sf.openrocket.simulation;
import net.sf.openrocket.masscalc.MassData;
import net.sf.openrocket.models.atmosphere.AtmosphericConditions;
import net.sf.openrocket.simulation.exception.SimulationException;
import net.sf.openrocket.util.Coordinate;
@ -35,9 +35,9 @@ public class BasicTumbleStepper extends AbstractSimulationStepper {
// Compute drag force
double dynP = (0.5 * atmosphere.getDensity() * airSpeed.length2());
double dragForce = tumbleDrag * dynP;
// n.b. this is consntant, and could be calculated once at the beginning of this simulation branch...
MassData massData = calculateDryMassData(status);
double mass = massData.getCG().weight;
// n.b. this is constant, and could be calculated once at the beginning of this simulation branch...
double mass = calculateStructureMass(status).getMass();
// Compute drag acceleration

View File

@ -10,7 +10,7 @@ import net.sf.openrocket.aerodynamics.AerodynamicForces;
import net.sf.openrocket.aerodynamics.FlightConditions;
import net.sf.openrocket.aerodynamics.WarningSet;
import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.masscalc.MassData;
import net.sf.openrocket.masscalc.RigidBody;
import net.sf.openrocket.models.atmosphere.AtmosphericConditions;
import net.sf.openrocket.simulation.exception.SimulationCalculationException;
import net.sf.openrocket.simulation.exception.SimulationException;
@ -327,10 +327,10 @@ public class RK4SimulationStepper extends AbstractSimulationStepper {
calculateForces(status, store);
// Calculate mass data
MassData dryMassData = calculateDryMassData(status);
RigidBody structureMassData = calculateStructureMass(status);
store.propellantMassData = calculatePropellantMassData(status);
store.rocketMassData = dryMassData.add( store.propellantMassData );
store.motorMass = calculateMotorMass(status);
store.rocketMass = structureMassData.add( store.motorMass );
// Calculate the forces from the aerodynamic coefficients
@ -347,9 +347,9 @@ public class RK4SimulationStepper extends AbstractSimulationStepper {
double forceZ = store.thrustForce - store.dragForce;
store.linearAcceleration = new Coordinate(-fN / store.rocketMassData.getCG().weight,
-fSide / store.rocketMassData.getCG().weight,
forceZ / store.rocketMassData.getCG().weight);
store.linearAcceleration = new Coordinate(-fN / store.rocketMass.getMass(),
-fSide / store.rocketMass.getMass(),
forceZ / store.rocketMass.getMass());
store.linearAcceleration = store.thetaRotation.rotateZ(store.linearAcceleration);
@ -378,8 +378,8 @@ public class RK4SimulationStepper extends AbstractSimulationStepper {
} else {
// Shift moments to CG
double Cm = store.forces.getCm() - store.forces.getCN() * store.rocketMassData.getCG().x / refLength;
double Cyaw = store.forces.getCyaw() - store.forces.getCside() * store.rocketMassData.getCG().x / refLength;
double Cm = store.forces.getCm() - store.forces.getCN() * store.rocketMass.getCM().x / refLength;
double Cyaw = store.forces.getCyaw() - store.forces.getCside() * store.rocketMass.getCM().x / refLength;
// Compute moments
double momX = -Cyaw * dynP * refArea * refLength;
@ -387,9 +387,9 @@ public class RK4SimulationStepper extends AbstractSimulationStepper {
double momZ = store.forces.getCroll() * dynP * refArea * refLength;
// Compute acceleration in rocket coordinates
store.angularAcceleration = new Coordinate(momX / store.rocketMassData.getLongitudinalInertia(),
momY / store.rocketMassData.getLongitudinalInertia(),
momZ / store.rocketMassData.getRotationalInertia());
store.angularAcceleration = new Coordinate(momX / store.rocketMass.getLongitudinalInertia(),
momY / store.rocketMass.getLongitudinalInertia(),
momZ / store.rocketMass.getRotationalInertia());
store.rollAcceleration = store.angularAcceleration.z;
// TODO: LOW: This should be hypot, but does it matter?
@ -597,30 +597,30 @@ public class RK4SimulationStepper extends AbstractSimulationStepper {
data.setValue(FlightDataType.TYPE_MACH_NUMBER, store.flightConditions.getMach());
}
if (store.rocketMassData != null) {
data.setValue(FlightDataType.TYPE_CG_LOCATION, store.rocketMassData.getCG().x);
if (store.rocketMass != null) {
data.setValue(FlightDataType.TYPE_CG_LOCATION, store.rocketMass.getCM().x);
}
if (status.isLaunchRodCleared()) {
// Don't include CP and stability with huge launch AOA
if (store.forces != null) {
data.setValue(FlightDataType.TYPE_CP_LOCATION, store.forces.getCP().x);
}
if (store.forces != null && store.flightConditions != null && store.rocketMassData != null) {
if (store.forces != null && store.flightConditions != null && store.rocketMass != null) {
data.setValue(FlightDataType.TYPE_STABILITY,
(store.forces.getCP().x - store.rocketMassData.getCG().x) / store.flightConditions.getRefLength());
(store.forces.getCP().x - store.rocketMass.getCM().x) / store.flightConditions.getRefLength());
}
}
if( null != store.propellantMassData ){
data.setValue(FlightDataType.TYPE_PROPELLANT_MASS, store.propellantMassData.getCG().weight);
if( null != store.motorMass ){
data.setValue(FlightDataType.TYPE_PROPELLANT_MASS, store.motorMass.getMass());
//data.setValue(FlightDataType.TYPE_PROPELLANT_LONGITUDINAL_INERTIA, store.propellantMassData.getLongitudinalInertia());
//data.setValue(FlightDataType.TYPE_PROPELLANT_ROTATIONAL_INERTIA, store.propellantMassData.getRotationalInertia());
}
if (store.rocketMassData != null) {
if (store.rocketMass != null) {
// N.B.: These refer to total mass
data.setValue(FlightDataType.TYPE_MASS, store.rocketMassData.getCG().weight);
data.setValue(FlightDataType.TYPE_LONGITUDINAL_INERTIA, store.rocketMassData.getLongitudinalInertia());
data.setValue(FlightDataType.TYPE_ROTATIONAL_INERTIA, store.rocketMassData.getRotationalInertia());
data.setValue(FlightDataType.TYPE_MASS, store.rocketMass.getMass());
data.setValue(FlightDataType.TYPE_LONGITUDINAL_INERTIA, store.rocketMass.getLongitudinalInertia());
data.setValue(FlightDataType.TYPE_ROTATIONAL_INERTIA, store.rocketMass.getRotationalInertia());
}
data.setValue(FlightDataType.TYPE_THRUST_FORCE, store.thrustForce);
@ -628,11 +628,11 @@ public class RK4SimulationStepper extends AbstractSimulationStepper {
data.setValue(FlightDataType.TYPE_GRAVITY, store.gravity);
if (status.isLaunchRodCleared() && store.forces != null) {
if (store.rocketMassData != null && store.flightConditions != null) {
if (store.rocketMass != null && store.flightConditions != null) {
data.setValue(FlightDataType.TYPE_PITCH_MOMENT_COEFF,
store.forces.getCm() - store.forces.getCN() * store.rocketMassData.getCG().x / store.flightConditions.getRefLength());
store.forces.getCm() - store.forces.getCN() * store.rocketMass.getCM().x / store.flightConditions.getRefLength());
data.setValue(FlightDataType.TYPE_YAW_MOMENT_COEFF,
store.forces.getCyaw() - store.forces.getCside() * store.rocketMassData.getCG().x / store.flightConditions.getRefLength());
store.forces.getCyaw() - store.forces.getCside() * store.rocketMass.getCM().x / store.flightConditions.getRefLength());
}
data.setValue(FlightDataType.TYPE_NORMAL_FORCE_COEFF, store.forces.getCN());
data.setValue(FlightDataType.TYPE_SIDE_FORCE_COEFF, store.forces.getCside());
@ -715,9 +715,9 @@ public class RK4SimulationStepper extends AbstractSimulationStepper {
public double longitudinalAcceleration = Double.NaN;
public MassData rocketMassData;
public RigidBody rocketMass;
public MassData propellantMassData;
public RigidBody motorMass;
public Coordinate coriolisAcceleration;

View File

@ -7,6 +7,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
import net.sf.openrocket.aerodynamics.FlightConditions;
import net.sf.openrocket.aerodynamics.WarningSet;
import net.sf.openrocket.motor.MotorConfiguration;
@ -101,7 +102,6 @@ public class SimulationStatus implements Monitorable {
// Initialize to roll angle with least stability w.r.t. the wind
Quaternion o;
FlightConditions cond = new FlightConditions(this.configuration);
this.simulationConditions.getAerodynamicCalculator().getWorstCP(this.configuration, cond, null);
double angle = -cond.getTheta() - (Math.PI / 2.0 - this.simulationConditions.getLaunchRodDirection());
o = Quaternion.rotation(new Coordinate(0, 0, angle));

View File

@ -11,7 +11,7 @@ import org.slf4j.LoggerFactory;
import net.sf.openrocket.aerodynamics.AerodynamicForces;
import net.sf.openrocket.aerodynamics.FlightConditions;
import net.sf.openrocket.masscalc.MassData;
import net.sf.openrocket.masscalc.RigidBody;
import net.sf.openrocket.models.atmosphere.AtmosphericConditions;
import net.sf.openrocket.motor.MotorConfigurationId;
import net.sf.openrocket.rocketcomponent.MotorMount;
@ -144,8 +144,8 @@ public class ScriptingSimulationListener implements SimulationListener, Simulati
}
@Override
public MassData preMassCalculation(SimulationStatus status) throws SimulationException {
return invoke(MassData.class, null, "preMassCalculation", status);
public RigidBody preMassCalculation(SimulationStatus status) throws SimulationException {
return invoke(RigidBody.class, null, "preMassCalculation", status);
}
@Override
@ -184,8 +184,8 @@ public class ScriptingSimulationListener implements SimulationListener, Simulati
}
@Override
public MassData postMassCalculation(SimulationStatus status, MassData massData) throws SimulationException {
return invoke(MassData.class, null, "postMassCalculation", status, massData);
public RigidBody postMassCalculation(SimulationStatus status, RigidBody RigidBody) throws SimulationException {
return invoke(RigidBody.class, null, "postMassCalculation", status, RigidBody);
}
@Override

View File

@ -2,7 +2,7 @@ package net.sf.openrocket.simulation.listeners;
import net.sf.openrocket.aerodynamics.AerodynamicForces;
import net.sf.openrocket.aerodynamics.FlightConditions;
import net.sf.openrocket.masscalc.MassData;
import net.sf.openrocket.masscalc.RigidBody;
import net.sf.openrocket.models.atmosphere.AtmosphericConditions;
import net.sf.openrocket.motor.MotorConfigurationId;
import net.sf.openrocket.rocketcomponent.MotorMount;
@ -111,7 +111,7 @@ public class AbstractSimulationListener implements SimulationListener, Simulatio
}
@Override
public MassData preMassCalculation(SimulationStatus status) throws SimulationException {
public RigidBody preMassCalculation(SimulationStatus status) throws SimulationException {
return null;
}
@ -151,7 +151,7 @@ public class AbstractSimulationListener implements SimulationListener, Simulatio
}
@Override
public MassData postMassCalculation(SimulationStatus status, MassData massData) throws SimulationException {
public RigidBody postMassCalculation(SimulationStatus status, RigidBody RigidBody) throws SimulationException {
return null;
}

View File

@ -2,7 +2,7 @@ package net.sf.openrocket.simulation.listeners;
import net.sf.openrocket.aerodynamics.AerodynamicForces;
import net.sf.openrocket.aerodynamics.FlightConditions;
import net.sf.openrocket.masscalc.MassData;
import net.sf.openrocket.masscalc.RigidBody;
import net.sf.openrocket.models.atmosphere.AtmosphericConditions;
import net.sf.openrocket.simulation.AccelerationData;
import net.sf.openrocket.simulation.SimulationStatus;
@ -55,9 +55,9 @@ public interface SimulationComputationListener extends SimulationListener {
public AerodynamicForces postAerodynamicCalculation(SimulationStatus status, AerodynamicForces forces)
throws SimulationException;
public MassData preMassCalculation(SimulationStatus status) throws SimulationException;
public RigidBody preMassCalculation(SimulationStatus status) throws SimulationException;
public MassData postMassCalculation(SimulationStatus status, MassData massData) throws SimulationException;
public RigidBody postMassCalculation(SimulationStatus status, RigidBody massData) throws SimulationException;
public double preSimpleThrustCalculation(SimulationStatus status) throws SimulationException;

View File

@ -7,7 +7,7 @@ import org.slf4j.LoggerFactory;
import net.sf.openrocket.aerodynamics.AerodynamicForces;
import net.sf.openrocket.aerodynamics.FlightConditions;
import net.sf.openrocket.aerodynamics.Warning;
import net.sf.openrocket.masscalc.MassData;
import net.sf.openrocket.masscalc.RigidBody;
import net.sf.openrocket.models.atmosphere.AtmosphericConditions;
import net.sf.openrocket.motor.MotorConfigurationId;
import net.sf.openrocket.rocketcomponent.MotorMount;
@ -502,9 +502,9 @@ public class SimulationListenerHelper {
*
* @return <code>null</code> normally, or overriding mass data.
*/
public static MassData firePreMassCalculation(SimulationStatus status)
public static RigidBody firePreMassCalculation(SimulationStatus status)
throws SimulationException {
MassData mass;
RigidBody mass;
int modID = status.getModID();
for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) {
@ -528,8 +528,8 @@ public class SimulationListenerHelper {
*
* @return the resultant mass data
*/
public static MassData firePostMassCalculation(SimulationStatus status, MassData mass) throws SimulationException {
MassData m;
public static RigidBody firePostMassCalculation(SimulationStatus status, RigidBody mass) throws SimulationException {
RigidBody m;
int modID = status.getModID();
for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) {

View File

@ -336,6 +336,11 @@ public final class Coordinate implements Cloneable, Serializable {
return String.format("(%.3f,%.3f,%.3f)", x, y, z);
}
// high-precision output, for use with verifying calculations
public String toPreciseString() {
return String.format("cm= %.8fg @[%.8f,%.8f,%.8f]", weight, x, y, z);
}
@Override
public Coordinate clone() {
return new Coordinate(this.x, this.y, this.z, this.weight);

View File

@ -212,36 +212,6 @@ public class TestRockets {
.build();
}
//
public static Rocket makeNoMotorRocket() {
Rocket rocket;
AxialStage stage;
NoseCone nosecone;
BodyTube bodytube;
rocket = new Rocket();
stage = new AxialStage();
stage.setName("Stage1");
// Stage construction
rocket.addChild(stage);
nosecone = new NoseCone(Transition.Shape.ELLIPSOID, 0.105, 0.033);
nosecone.setThickness(0.001);
bodytube = new BodyTube(0.69, 0.033, 0.001);
bodytube.setMotorMount(true);
TrapezoidFinSet finset = new TrapezoidFinSet(3, 0.495, 0.1, 0.3, 0.185);
finset.setThickness(0.005);
bodytube.addChild(finset);
// Component construction
stage.addChild(nosecone);
stage.addChild(bodytube);
rocket.enableEvents();
return rocket;
}
/**
* Create a new test rocket based on the value 'key'. The rocket utilizes most of the
* properties and features available. The same key always returns the same rocket,
@ -877,7 +847,11 @@ public class TestRockets {
return rocket;
}
public final static String FALCON_9_FCID_1="test_config #1: [ M1350, G77]";
public final static String FALCON_9H_FCID_1="test_config #1: [ M1350, G77]";
public final static int FALCON_9H_PAYLOAD_STAGE_NUMBER=0;
public final static int FALCON_9H_CORE_STAGE_NUMBER=1;
public final static int FALCON_9H_BOOSTER_STAGE_NUMBER=2;
// This function is used for unit, integration tests, DO NOT CHANGE (without updating tests).
@ -885,7 +859,7 @@ public class TestRockets {
Rocket rocket = new Rocket();
rocket.setName("Falcon9H Scale Rocket");
FlightConfigurationId selFCID = rocket.createFlightConfiguration( new FlightConfigurationId( FALCON_9_FCID_1 ));
FlightConfigurationId selFCID = rocket.createFlightConfiguration( new FlightConfigurationId( FALCON_9H_FCID_1 ));
rocket.setSelectedConfiguration(selFCID);
// ====== Payload Stage ======

View File

@ -122,7 +122,6 @@ public class Transformation implements java.io.Serializable {
return new Coordinate(x,y,z,orig.weight);
}
/**
* Transform an array of coordinates. The transformed coordinates are stored
* in the same array, and the array is returned.
@ -255,6 +254,13 @@ public class Transformation implements java.io.Serializable {
}
public boolean isIdentity() {
if( this == Transformation.IDENTITY ) {
return true;
}
return this.equals( Transformation.IDENTITY );
}
public void print(String... str) {
for (String s: str) {
@ -365,4 +371,8 @@ public class Transformation implements java.io.Serializable {
return DoubleBuffer.wrap(data);
}
public Coordinate getTranslationVector() {
return this.translate;
}
}

View File

@ -0,0 +1,74 @@
package net.sf.openrocket.masscalc;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import net.sf.openrocket.rocketcomponent.Rocket;
import net.sf.openrocket.util.TestRockets;
import net.sf.openrocket.util.BaseTestCase.BaseTestCase;
public class MassCacheTest extends BaseTestCase {
@Test
public void testCMCache() {
Rocket rocket = TestRockets.makeFalcon9Heavy();
rocket.setName("TestRocket."+Thread.currentThread().getStackTrace()[1].getMethodName());
// ant throws in error if it can't find a test case in the *Test.java file.
// .... soooo we have this waste of space. -DMW
assertTrue( true );
}
//
// FlightConfiguration config = rocket.getEmptyConfiguration();
// MassCalculator mc = new MassCalculator();
//
// {
// // validate payload stage
// AxialStage payloadStage = (AxialStage) rocket.getChild(0);
// int plNum = payloadStage.getStageNumber();
// config.setOnlyStage( plNum );
//
// MassData calcMass = mc.calculateBurnoutMassData( config );
//
// double expMass = 0.116287;
// double expCMx = 0.278070785749;
// assertEquals("Upper Stage Mass is incorrect: ", expMass, calcMass.getCM().weight, EPSILON);
// assertEquals("Upper Stage CM.x is incorrect: ", expCMx, calcMass.getCM().x, EPSILON);
// assertEquals("Upper Stage CM.y is incorrect: ", 0.0f, calcMass.getCM().y, EPSILON);
// assertEquals("Upper Stage CM.z is incorrect: ", 0.0f, calcMass.getCM().z, EPSILON);
//
// MassData rocketLaunchMass = mc.getRocketLaunchMassData( config);
// assertEquals("Upper Stage Mass (cache) is incorrect: ", expMass, rocketLaunchMass.getCM().weight, EPSILON);
// assertEquals("Upper Stage CM.x (cache) is incorrect: ", expCMx, rocketLaunchMass.getCM().x, EPSILON);
//
// MassData rocketSpentMass = mc.getRocketSpentMassData( config);
// assertEquals("Upper Stage Mass (cache) is incorrect: ", expMass, rocketSpentMass.getCM().weight, EPSILON);
// assertEquals("Upper Stage CM.x (cache) is incorrect: ", expCMx, rocketSpentMass.getCM().x, EPSILON);
// }{
// ParallelStage boosters = (ParallelStage) rocket.getChild(1).getChild(1);
// int boostNum = boosters.getStageNumber();
// config.setOnlyStage( boostNum );
//
// MassData boosterMass = mc.calculateBurnoutMassData( config);
//
// double expMass = BOOSTER_SET_NO_MOTORS_MASS;
// double expCMx = BOOSTER_SET_NO_MOTORS_CMX;
// assertEquals("Heavy Booster Mass is incorrect: ", expMass, boosterMass.getCM().weight, EPSILON);
// assertEquals("Heavy Booster CM.x is incorrect: ", expCMx, boosterMass.getCM().x, EPSILON);
// assertEquals("Heavy Booster CM.y is incorrect: ", 0.0f, boosterMass.getCM().y, EPSILON);
// assertEquals("Heavy Booster CM.z is incorrect: ", 0.0f, boosterMass.getCM().z, EPSILON);
//
// MassData rocketLaunchMass = mc.getRocketLaunchMassData( config);
// assertEquals(" Booster Stage Mass (cache) is incorrect: ", expMass, rocketLaunchMass.getCM().weight, EPSILON);
// assertEquals(" Booster Stage CM.x (cache) is incorrect: ", expCMx, rocketLaunchMass.getCM().x, EPSILON);
//
// MassData rocketSpentMass = mc.getRocketSpentMassData( config);
// assertEquals(" Booster Stage Mass (cache) is incorrect: ", expMass, rocketSpentMass.getCM().weight, EPSILON);
// assertEquals(" Booster Stage CM.x (cache) is incorrect: ", expCMx, rocketSpentMass.getCM().x, EPSILON);
// }
// }
//
}

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,8 @@ import net.sf.openrocket.util.Coordinate;
import net.sf.openrocket.util.MathUtil;
import net.sf.openrocket.util.BaseTestCase.BaseTestCase;
public class MassDataTest extends BaseTestCase {
public class RigidBodyTest extends BaseTestCase {
// tolerance for compared double test results
protected final double EPSILON = MathUtil.EPSILON;
@ -22,20 +23,21 @@ public class MassDataTest extends BaseTestCase {
Coordinate r1 = new Coordinate(0,-40, 0, m1);
double I1ax=28.7;
double I1t = I1ax/2;
MassData body1 = new MassData(r1, I1ax, I1t);
RigidBody body1 = new RigidBody(r1, I1ax, I1t);
double m2 = 5.7;
Coordinate r2 = new Coordinate(0, 32, 0, m2);
double I2ax=20;
double I2t = I2ax/2;
MassData body2 = new MassData(r2, I2ax, I2t);
RigidBody body2 = new RigidBody(r2, I2ax, I2t);
// point 3 is defined as the CM of bodies 1 and 2 combined.
MassData asbly3 = body1.add(body2);
RigidBody asbly3 = body1.add(body2);
Coordinate cm3_expected = r1.average(r2);
assertEquals(" Center of Mass calculated incorrectly: ", cm3_expected, asbly3.getCM() );
// these are a bit of a hack, and depend upon all the bodies being along the y=0, z=0 line.
Coordinate delta13 = asbly3.getCM().sub( r1);
Coordinate delta23 = asbly3.getCM().sub( r2);
@ -68,16 +70,16 @@ public class MassDataTest extends BaseTestCase {
Coordinate r1 = new Coordinate(0,-40, -10, m1);
double I1xx=28.7;
double I1t = I1xx/2;
MassData body1 = new MassData(r1, I1xx, I1t);
RigidBody body1 = new RigidBody(r1, I1xx, I1t);
double m2 = 5.7;
Coordinate r2 = new Coordinate(0, 32, 15, m2);
double I2xx=20;
double I2t = I2xx/2;
MassData body2 = new MassData(r2, I2xx, I2t);
RigidBody body2 = new RigidBody(r2, I2xx, I2t);
// point 3 is defined as the CM of bodies 1 and 2 combined.
MassData asbly3 = body1.add(body2);
RigidBody asbly3 = body1.add(body2);
Coordinate cm3_expected = r1.average(r2);
assertEquals(" Center of Mass calculated incorrectly: ", cm3_expected, asbly3.getCM() );
@ -113,30 +115,30 @@ public class MassDataTest extends BaseTestCase {
@Test
public void testMassDataCompoundCalculations() {
public void testRigidBodyCompoundCalculations() {
double m1 = 2.5;
Coordinate r1 = new Coordinate(0,-40, 0, m1);
double I1ax=28.7;
double I1t = I1ax/2;
MassData body1 = new MassData(r1, I1ax, I1t);
RigidBody body1 = new RigidBody(r1, I1ax, I1t);
double m2 = m1;
Coordinate r2 = new Coordinate(0, -2, 0, m2);
double I2ax=28.7;
double I2t = I2ax/2;
MassData body2 = new MassData(r2, I2ax, I2t);
RigidBody body2 = new RigidBody(r2, I2ax, I2t);
double m5 = 5.7;
Coordinate r5 = new Coordinate(0, 32, 0, m5);
double I5ax=20;
double I5t = I5ax/2;
MassData body5 = new MassData(r5, I5ax, I5t);
RigidBody body5 = new RigidBody(r5, I5ax, I5t);
// point 3 is defined as the CM of bodies 1 and 2 combined.
MassData asbly3 = body1.add(body2);
RigidBody asbly3 = body1.add(body2);
// point 4 is defined as the CM of bodies 1, 2 and 5 combined.
MassData asbly4_indirect = asbly3.add(body5);
RigidBody asbly4_indirect = asbly3.add(body5);
Coordinate cm4_expected = r1.average(r2).average(r5);
assertEquals(" Center of Mass calculated incorrectly: ", cm4_expected, new Coordinate( 0, 7.233644859813085, 0, m1+m2+m5 ) );
@ -154,14 +156,11 @@ public class MassDataTest extends BaseTestCase {
double I4xx = I14ax+I24ax+I54ax;
double I4yy = I1t+I2t+I5t;
double I4zz = I14zz+I24zz+I54zz;
MassData asbly4_expected = new MassData( cm4_expected, I4xx, I4yy, I4zz);
RigidBody asbly4_expected = new RigidBody( cm4_expected, I4xx, I4yy, I4zz);
assertEquals("x-axis MOI don't match: ", asbly4_indirect.getIxx(), asbly4_expected.getIxx(), EPSILON*10);
assertEquals("y-axis MOI don't match: ", asbly4_indirect.getIyy(), asbly4_expected.getIyy(), EPSILON*10);
assertEquals("z-axis MOI don't match: ", asbly4_indirect.getIzz(), asbly4_expected.getIzz(), EPSILON*10);
}
}

View File

@ -13,7 +13,6 @@ import net.sf.openrocket.util.TestRockets;
import net.sf.openrocket.util.BaseTestCase.BaseTestCase;
public class RocketTest extends BaseTestCase {
final double EPSILON = MathUtil.EPSILON;
@Test
@ -125,9 +124,6 @@ public class RocketTest extends BaseTestCase {
ring.setInstanceCount(2);
Coordinate actLocs[] = ring.getComponentLocations();
{ // first instance
// assertEquals(" position x fail: ", expLoc.x, actLoc.x, EPSILON);
// assertEquals(" position y fail: ", expLoc.y, actLoc.y, EPSILON);
// assertEquals(" position z fail: ", expLoc.z, actLoc.z, EPSILON);
expLoc = new Coordinate(0.21, 0, 0);
actLoc = actLocs[0];
assertThat(ring.getName()+" not positioned correctly: ", actLoc, equalTo(expLoc));
@ -179,4 +175,100 @@ public class RocketTest extends BaseTestCase {
}
}
public void testFalcon9HComponentLocations() {
Rocket rkt = TestRockets.makeFalcon9Heavy();
rkt.setName("TestRocket."+Thread.currentThread().getStackTrace()[1].getMethodName());
Coordinate offset;
Coordinate loc;
// ====== Payload Stage ======
// ====== ====== ====== ======
{
NoseCone nc = (NoseCone)rkt.getChild(0).getChild(0);
offset = nc.getOffset();
loc = nc.getComponentLocations()[0];
assertEquals("P/L NoseCone offset is incorrect: ", 0.0, offset.x, EPSILON);
assertEquals("P/L NoseCone location is incorrect: ", 0.0, loc.x, EPSILON);
BodyTube plbody = (BodyTube)rkt.getChild(0).getChild(1);
offset = plbody.getOffset();
loc = plbody.getComponentLocations()[0];
assertEquals("P/L Body offset calculated incorrectly: ", 0.118, offset.x, EPSILON);
assertEquals("P/L Body location calculated incorrectly: ", 0.118, loc.x, EPSILON);
Transition tr= (Transition)rkt.getChild(0).getChild(2);
offset = tr.getOffset();
loc = tr.getComponentLocations()[0];
assertEquals(tr.getName()+" offset is incorrect: ", 0.250, offset.x, EPSILON);
assertEquals(tr.getName()+" location is incorrect: ", 0.250, loc.x, EPSILON);
BodyTube upperBody = (BodyTube)rkt.getChild(0).getChild(3);
offset = upperBody.getOffset();
loc = upperBody.getComponentLocations()[0];
assertEquals(upperBody.getName()+" offset is incorrect: ", 0.264, offset.x, EPSILON);
assertEquals(upperBody.getName()+" location is incorrect: ", 0.264, loc.x, EPSILON);
{
Parachute chute = (Parachute)rkt.getChild(0).getChild(3).getChild(0);
offset = chute.getOffset();
loc = chute.getComponentLocations()[0];
assertEquals(chute.getName()+" offset is incorrect: ", 0.0775, offset.x, EPSILON);
assertEquals(chute.getName()+" location is incorrect: ", 0.3415, loc.x, EPSILON);
ShockCord cord= (ShockCord)rkt.getChild(0).getChild(3).getChild(1);
offset = cord.getOffset();
loc = cord.getComponentLocations()[0];
assertEquals(cord.getName()+" offset is incorrect: ", 0.155, offset.x, EPSILON);
assertEquals(cord.getName()+" location is incorrect: ", 0.419, loc.x, EPSILON);
}
BodyTube interstage = (BodyTube)rkt.getChild(0).getChild(4);
offset = interstage.getOffset();
loc = interstage.getComponentLocations()[0];
assertEquals(interstage.getName()+" offset is incorrect: ", 0.444, offset.x, EPSILON);
assertEquals(interstage.getName()+" location is incorrect: ", 0.444, loc.x, EPSILON);
}
// ====== Core Stage ======
// ====== ====== ======
{
BodyTube coreBody = (BodyTube)rkt.getChild(1).getChild(0);
offset = coreBody.getOffset();
loc = coreBody.getComponentLocations()[0];
assertEquals(coreBody.getName()+" offset is incorrect: ", 0.0, offset.x, EPSILON);
assertEquals(coreBody.getName()+" location is incorrect: ", 0.564, loc.x, EPSILON);
FinSet coreFins = (FinSet)rkt.getChild(1).getChild(0).getChild(0);
offset = coreFins.getOffset();
loc = coreFins.getComponentLocations()[0];
assertEquals(coreFins.getName()+" offset is incorrect: ", 0.480, offset.x, EPSILON);
assertEquals(coreFins.getName()+" location is incorrect: ", 1.044, loc.x, EPSILON);
}
// ====== Booster Set Stage ======
// ====== ====== ======
ParallelStage boosters = (ParallelStage) rkt.getChild(1).getChild(1);
{
// think of the casts as an assert that ( child instanceof NoseCone) == true
NoseCone nose = (NoseCone) boosters.getChild(0);
offset = nose.getOffset();
loc = nose.getComponentLocations()[0];
assertEquals(nose.getName()+" offset is incorrect: ", 0.0, offset.x, EPSILON);
assertEquals(nose.getName()+" location is incorrect: ", 0.484, loc.x, EPSILON);
BodyTube boosterBody= (BodyTube) boosters.getChild(1);
offset = boosterBody.getOffset();
loc = boosterBody.getComponentLocations()[0];
assertEquals(boosterBody.getName()+" offset is incorrect: ", 0.08, offset.x, EPSILON);
assertEquals(boosterBody.getName()+" location is incorrect: ", 0.564, loc.x, EPSILON);
InnerTube mmt = (InnerTube)boosters.getChild(1).getChild(0);
offset = mmt.getOffset();
loc = mmt.getComponentLocations()[0];
assertEquals(mmt.getName()+" offset is incorrect: ", 0.65, offset.x, EPSILON);
assertEquals(mmt.getName()+" location is incorrect: ", 1.214, loc.x, EPSILON);
}
}
}

View File

@ -340,7 +340,7 @@ public class InnerTubeConfig extends RocketComponentConfig {
}
InnerTube tube = (InnerTube) component;
if (tube.getClusterCount() <= 1)
if (tube.getInstanceCount() <= 1)
return;
document.addUndoPosition("Split cluster");

View File

@ -74,7 +74,7 @@ public class PrintableCenteringRing extends AbstractPrintable<CenteringRing> {
List<Coordinate> points = new ArrayList<Coordinate>();
//Transform the radial positions of the tubes.
for (InnerTube it : theMotorMounts) {
if (it.getClusterCount() > 1) {
if (it.getInstanceCount() > 1) {
List<Coordinate> c = it.getClusterPoints();
for (Coordinate coordinate : c) {
points.add(coordinate.setX(it.getOuterRadius()));

View File

@ -52,7 +52,7 @@ import net.sf.openrocket.gui.simulation.SimulationWorker;
import net.sf.openrocket.gui.util.SwingPreferences;
import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.masscalc.MassCalculator;
import net.sf.openrocket.masscalc.MassData;
import net.sf.openrocket.masscalc.RigidBody;
import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
import net.sf.openrocket.rocketcomponent.FlightConfiguration;
@ -125,8 +125,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change
/* Calculation of CP and CG */
private AerodynamicCalculator aerodynamicCalculator;
private MassCalculator massCalculator;
private final OpenRocketDocument document;
private Caret extraCP = null;
@ -180,8 +179,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change
// TODO: FUTURE: calculator selection
aerodynamicCalculator = new BarrowmanCalculator();
massCalculator = new MassCalculator();
// Create figure and custom scroll pane
figure = new RocketFigure(rkt);
figure3d = new RocketFigure3d(document);
@ -595,7 +593,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change
}
extraText.setTheta(cpTheta);
cg = massCalculator.getRocketLaunchMassData( curConfig).getCG();
cg = MassCalculator.calculateLaunch( curConfig).getCM();
if (cp.weight > MassCalculator.MIN_MASS){
cpx = cp.x;
@ -634,8 +632,8 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change
}
}
MassData emptyInfo = massCalculator.getRocketSpentMassData( curConfig.getRocket().getEmptyConfiguration());
RigidBody emptyInfo = MassCalculator.calculateStructure( curConfig );
extraText.setCG(cgx);
extraText.setCP(cpx);
extraText.setLength(length);

View File

@ -43,11 +43,13 @@ import net.sf.openrocket.gui.main.UndoRedoAction;
import net.sf.openrocket.l10n.DebugTranslator;
import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.masscalc.MassCalculator;
import net.sf.openrocket.masscalc.RigidBody;
import net.sf.openrocket.motor.ThrustCurveMotor;
import net.sf.openrocket.plugin.PluginModule;
import net.sf.openrocket.rocketcomponent.EngineBlock;
import net.sf.openrocket.rocketcomponent.FlightConfiguration;
import net.sf.openrocket.rocketcomponent.FlightConfigurationId;
import net.sf.openrocket.rocketcomponent.InnerTube;
import net.sf.openrocket.rocketcomponent.MassComponent;
import net.sf.openrocket.rocketcomponent.NoseCone;
import net.sf.openrocket.rocketcomponent.RocketComponent;
@ -69,7 +71,6 @@ public class IntegrationTest {
private Action undoAction, redoAction;
private AerodynamicCalculator aeroCalc = new BarrowmanCalculator();
private MassCalculator massCalc = new MassCalculator();
private FlightConfiguration config;
private FlightConditions conditions;
private String massComponentID = null;
@ -118,7 +119,13 @@ public class IntegrationTest {
// Test undo state
checkUndoState(null, null);
InnerTube mmt = (InnerTube)config.getRocket().getChild(0).getChild(1).getChild(2);
System.err.println(String.format("IntegrationTest::testSimpleRocket(...)...."));
System.err.println(String.format(" Config: %s", config.toDebug() ));
System.err.println(String.format(" motor config: %s", mmt.getMotorConfig( config.getId() ).toDescription() ));
// Compute cg+cp + altitude
// double cgx, double mass, double cpx, double cna)
checkCgCp(0.248, 0.0645, 0.320, 12.0);
checkAlt(48.2);
@ -326,13 +333,12 @@ public class IntegrationTest {
}
private void checkCgCp(double cgx, double mass, double cpx, double cna) {
Coordinate cg, cp;
cg = massCalc.getRocketLaunchMassData(config).getCG();
final RigidBody launchData = MassCalculator.calculateLaunch(config);
final Coordinate cg = launchData.getCenterOfMass();
assertEquals(cgx, cg.x, 0.001);
assertEquals(mass, cg.weight, 0.0005);
cp = aeroCalc.getWorstCP(config, conditions, null);
final Coordinate cp = aeroCalc.getWorstCP(config, conditions, null);
assertEquals(cpx, cp.x, 0.001);
assertEquals(cna, cp.weight, 0.1);
}