From 20eff575f4d96ea06ef6da43188c91b646d5a584 Mon Sep 17 00:00:00 2001 From: Daniel_M_Williams Date: Sat, 9 Dec 2017 15:22:04 -0500 Subject: [PATCH] [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 --- .../file/rocksim/export/BodyTubeDTO.java | 2 +- .../file/rocksim/export/InnerBodyTubeDTO.java | 2 +- .../file/rocksim/export/RocksimSaver.java | 7 +- .../openrocket/masscalc/MassCalculation.java | 369 +++++++ .../openrocket/masscalc/MassCalculator.java | 446 ++------ .../net/sf/openrocket/masscalc/MassData.java | 269 ----- .../net/sf/openrocket/masscalc/RigidBody.java | 141 +++ .../domains/StabilityDomain.java | 4 +- .../parameters/StabilityParameter.java | 4 +- .../rocketcomponent/Clusterable.java | 4 - .../rocketcomponent/FlightConfiguration.java | 4 + .../openrocket/rocketcomponent/InnerTube.java | 14 +- .../rocketcomponent/RingComponent.java | 10 +- .../rocketcomponent/RocketComponent.java | 42 +- .../simulation/AbstractSimulationStepper.java | 52 +- .../simulation/BasicLandingStepper.java | 4 +- .../simulation/BasicTumbleStepper.java | 8 +- .../simulation/RK4SimulationStepper.java | 54 +- .../simulation/SimulationStatus.java | 2 +- .../impl/ScriptingSimulationListener.java | 10 +- .../listeners/AbstractSimulationListener.java | 6 +- .../SimulationComputationListener.java | 6 +- .../listeners/SimulationListenerHelper.java | 10 +- .../net/sf/openrocket/util/Coordinate.java | 5 + .../net/sf/openrocket/util/TestRockets.java | 38 +- .../sf/openrocket/util/Transformation.java | 12 +- .../sf/openrocket/masscalc/MassCacheTest.java | 74 ++ .../masscalc/MassCalculatorTest.java | 961 +++++++++--------- .../{MassDataTest.java => RigidBodyTest.java} | 35 +- .../rocketcomponent/RocketTest.java | 100 +- .../gui/configdialog/InnerTubeConfig.java | 2 +- .../gui/print/PrintableCenteringRing.java | 2 +- .../gui/scalefigure/RocketPanel.java | 14 +- .../net/sf/openrocket/IntegrationTest.java | 16 +- 34 files changed, 1414 insertions(+), 1315 deletions(-) create mode 100644 core/src/net/sf/openrocket/masscalc/MassCalculation.java delete mode 100644 core/src/net/sf/openrocket/masscalc/MassData.java create mode 100644 core/src/net/sf/openrocket/masscalc/RigidBody.java create mode 100644 core/test/net/sf/openrocket/masscalc/MassCacheTest.java rename core/test/net/sf/openrocket/masscalc/{MassDataTest.java => RigidBodyTest.java} (87%) diff --git a/core/src/net/sf/openrocket/file/rocksim/export/BodyTubeDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/BodyTubeDTO.java index 97e404d95..507f59432 100644 --- a/core/src/net/sf/openrocket/file/rocksim/export/BodyTubeDTO.java +++ b/core/src/net/sf/openrocket/file/rocksim/export/BodyTubeDTO.java @@ -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) { diff --git a/core/src/net/sf/openrocket/file/rocksim/export/InnerBodyTubeDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/InnerBodyTubeDTO.java index 179fce838..9c23081e6 100644 --- a/core/src/net/sf/openrocket/file/rocksim/export/InnerBodyTubeDTO.java +++ b/core/src/net/sf/openrocket/file/rocksim/export/InnerBodyTubeDTO.java @@ -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) { diff --git a/core/src/net/sf/openrocket/file/rocksim/export/RocksimSaver.java b/core/src/net/sf/openrocket/file/rocksim/export/RocksimSaver.java index 099cc2448..175d7c246 100644 --- a/core/src/net/sf/openrocket/file/rocksim/export/RocksimSaver.java +++ b/core/src/net/sf/openrocket/file/rocksim/export/RocksimSaver.java @@ -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) { diff --git a/core/src/net/sf/openrocket/masscalc/MassCalculation.java b/core/src/net/sf/openrocket/masscalc/MassCalculation.java new file mode 100644 index 000000000..644371c61 --- /dev/null +++ b/core/src/net/sf/openrocket/masscalc/MassCalculation.java @@ -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) + * + */ +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 bodies = new ArrayList(); + + 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 ); + } + + +} + diff --git a/core/src/net/sf/openrocket/masscalc/MassCalculator.java b/core/src/net/sf/openrocket/masscalc/MassCalculator.java index 44e726fcb..eeaedb6c5 100644 --- a/core/src/net/sf/openrocket/masscalc/MassCalculator.java +++ b/core/src/net/sf/openrocket/masscalc/MassCalculator.java @@ -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(); - private MassData rocketSpentMassCache; - private MassData propellantMassCache; - + // private HashMap< Integer, MassData> stageMassCache = new HashMap(); + // 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 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 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 getCGAnalysis(FlightConfiguration configuration) { - revalidateCache(configuration); + // revalidateCache(configuration); Map map = new HashMap(); @@ -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 - * checkCache(), then {@link #voidMassCache()} is called. - *

- * 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 super.voidMassCache() during - * its execution. - */ - protected void voidMassCache() { - this.stageMassCache.clear(); - this.rocketSpentMassCache=null; - this.propellantMassCache=null; - } @Override public int getModID() { - return 0; + return this.modId; } } diff --git a/core/src/net/sf/openrocket/masscalc/MassData.java b/core/src/net/sf/openrocket/masscalc/MassData.java deleted file mode 100644 index a6fb68b34..000000000 --- a/core/src/net/sf/openrocket/masscalc/MassData.java +++ /dev/null @@ -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 - * @author Daniel Williams - */ -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 copy 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() + "]"; - } - -} diff --git a/core/src/net/sf/openrocket/masscalc/RigidBody.java b/core/src/net/sf/openrocket/masscalc/RigidBody.java new file mode 100644 index 000000000..01543c58b --- /dev/null +++ b/core/src/net/sf/openrocket/masscalc/RigidBody.java @@ -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 copy 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 ); + } + + + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/domains/StabilityDomain.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/domains/StabilityDomain.java index 338db8fb1..e33f8189e 100644 --- a/core/src/net/sf/openrocket/optimization/rocketoptimization/domains/StabilityDomain.java +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/domains/StabilityDomain.java @@ -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; diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/StabilityParameter.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/StabilityParameter.java index cce846453..08b67cbd3 100644 --- a/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/StabilityParameter.java +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/StabilityParameter.java @@ -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; diff --git a/core/src/net/sf/openrocket/rocketcomponent/Clusterable.java b/core/src/net/sf/openrocket/rocketcomponent/Clusterable.java index 8051fc13e..b93a65541 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/Clusterable.java +++ b/core/src/net/sf/openrocket/rocketcomponent/Clusterable.java @@ -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(); diff --git a/core/src/net/sf/openrocket/rocketcomponent/FlightConfiguration.java b/core/src/net/sf/openrocket/rocketcomponent/FlightConfiguration.java index 5807e8f34..b2079c153 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/FlightConfiguration.java +++ b/core/src/net/sf/openrocket/rocketcomponent/FlightConfiguration.java @@ -167,6 +167,10 @@ public class FlightConfiguration implements FlightConfigurableParameter getActiveComponents() { Queue toProcess = new ArrayDeque(this.getActiveStages()); ArrayList toReturn = new ArrayList<>(); diff --git a/core/src/net/sf/openrocket/rocketcomponent/InnerTube.java b/core/src/net/sf/openrocket/rocketcomponent/InnerTube.java index 216129cd5..58cbeaf4b 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/InnerTube.java +++ b/core/src/net/sf/openrocket/rocketcomponent/InnerTube.java @@ -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 getClusterPoints() { - List list = new ArrayList(getClusterCount()); + List list = new ArrayList(getInstanceCount()); List 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 points = getClusterPoints(); diff --git a/core/src/net/sf/openrocket/rocketcomponent/RingComponent.java b/core/src/net/sf/openrocket/rocketcomponent/RingComponent.java index bbf6b08ad..5dbf4d5ba 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/RingComponent.java +++ b/core/src/net/sf/openrocket/rocketcomponent/RingComponent.java @@ -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; } diff --git a/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java b/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java index afdf706b1..835226bca 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java +++ b/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java @@ -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 .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; + } } diff --git a/core/src/net/sf/openrocket/simulation/AbstractSimulationStepper.java b/core/src/net/sf/openrocket/simulation/AbstractSimulationStepper.java index 20307f419..3d753d547 100644 --- a/core/src/net/sf/openrocket/simulation/AbstractSimulationStepper.java +++ b/core/src/net/sf/openrocket/simulation/AbstractSimulationStepper.java @@ -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; } diff --git a/core/src/net/sf/openrocket/simulation/BasicLandingStepper.java b/core/src/net/sf/openrocket/simulation/BasicLandingStepper.java index 65636b432..cf2cf6e04 100644 --- a/core/src/net/sf/openrocket/simulation/BasicLandingStepper.java +++ b/core/src/net/sf/openrocket/simulation/BasicLandingStepper.java @@ -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 diff --git a/core/src/net/sf/openrocket/simulation/BasicTumbleStepper.java b/core/src/net/sf/openrocket/simulation/BasicTumbleStepper.java index 3b732fee0..f1e3795dc 100644 --- a/core/src/net/sf/openrocket/simulation/BasicTumbleStepper.java +++ b/core/src/net/sf/openrocket/simulation/BasicTumbleStepper.java @@ -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 diff --git a/core/src/net/sf/openrocket/simulation/RK4SimulationStepper.java b/core/src/net/sf/openrocket/simulation/RK4SimulationStepper.java index 7d137be15..ced30ce51 100644 --- a/core/src/net/sf/openrocket/simulation/RK4SimulationStepper.java +++ b/core/src/net/sf/openrocket/simulation/RK4SimulationStepper.java @@ -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; diff --git a/core/src/net/sf/openrocket/simulation/SimulationStatus.java b/core/src/net/sf/openrocket/simulation/SimulationStatus.java index 929880426..d8d48691b 100644 --- a/core/src/net/sf/openrocket/simulation/SimulationStatus.java +++ b/core/src/net/sf/openrocket/simulation/SimulationStatus.java @@ -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)); diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingSimulationListener.java b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingSimulationListener.java index ba916ca8a..6067378f4 100644 --- a/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingSimulationListener.java +++ b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingSimulationListener.java @@ -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 diff --git a/core/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java b/core/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java index 6529bc8e8..b1244fbc4 100644 --- a/core/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java +++ b/core/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java @@ -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; } diff --git a/core/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java b/core/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java index 5a3e3c95d..656d2fea2 100644 --- a/core/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java +++ b/core/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java @@ -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; diff --git a/core/src/net/sf/openrocket/simulation/listeners/SimulationListenerHelper.java b/core/src/net/sf/openrocket/simulation/listeners/SimulationListenerHelper.java index 6691ae2dc..903971481 100644 --- a/core/src/net/sf/openrocket/simulation/listeners/SimulationListenerHelper.java +++ b/core/src/net/sf/openrocket/simulation/listeners/SimulationListenerHelper.java @@ -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 null 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()) { diff --git a/core/src/net/sf/openrocket/util/Coordinate.java b/core/src/net/sf/openrocket/util/Coordinate.java index 0299bc21f..51c8b00f1 100644 --- a/core/src/net/sf/openrocket/util/Coordinate.java +++ b/core/src/net/sf/openrocket/util/Coordinate.java @@ -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); diff --git a/core/src/net/sf/openrocket/util/TestRockets.java b/core/src/net/sf/openrocket/util/TestRockets.java index 5e7731e41..8eb1c22e1 100644 --- a/core/src/net/sf/openrocket/util/TestRockets.java +++ b/core/src/net/sf/openrocket/util/TestRockets.java @@ -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 ====== diff --git a/core/src/net/sf/openrocket/util/Transformation.java b/core/src/net/sf/openrocket/util/Transformation.java index 3e76c9c7e..d31bb7906 100644 --- a/core/src/net/sf/openrocket/util/Transformation.java +++ b/core/src/net/sf/openrocket/util/Transformation.java @@ -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; + } + } diff --git a/core/test/net/sf/openrocket/masscalc/MassCacheTest.java b/core/test/net/sf/openrocket/masscalc/MassCacheTest.java new file mode 100644 index 000000000..15311a312 --- /dev/null +++ b/core/test/net/sf/openrocket/masscalc/MassCacheTest.java @@ -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); +// } +// } +// + +} diff --git a/core/test/net/sf/openrocket/masscalc/MassCalculatorTest.java b/core/test/net/sf/openrocket/masscalc/MassCalculatorTest.java index 5cd06e192..236617b1e 100644 --- a/core/test/net/sf/openrocket/masscalc/MassCalculatorTest.java +++ b/core/test/net/sf/openrocket/masscalc/MassCalculatorTest.java @@ -1,22 +1,25 @@ package net.sf.openrocket.masscalc; -import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; - import org.junit.Test; import net.sf.openrocket.motor.Motor; import net.sf.openrocket.rocketcomponent.AxialStage; import net.sf.openrocket.rocketcomponent.BodyTube; +import net.sf.openrocket.rocketcomponent.FinSet; import net.sf.openrocket.rocketcomponent.FlightConfiguration; import net.sf.openrocket.rocketcomponent.FlightConfigurationId; import net.sf.openrocket.rocketcomponent.InnerTube; import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.rocketcomponent.NoseCone; +import net.sf.openrocket.rocketcomponent.Parachute; import net.sf.openrocket.rocketcomponent.ParallelStage; import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.ShockCord; +import net.sf.openrocket.rocketcomponent.Transition; +import net.sf.openrocket.simulation.SimulationConditions; +import net.sf.openrocket.simulation.SimulationStatus; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.TestRockets; import net.sf.openrocket.util.BaseTestCase.BaseTestCase; @@ -26,50 +29,120 @@ public class MassCalculatorTest extends BaseTestCase { // tolerance for compared double test results private static final double EPSILON = 0.000001; - private static final double G77_MASS_LAUNCH = 0.123; - private static final double G77_MASS_SPENT = 0.064; - - - private static final double M1350_MASS_LAUNCH = 4.808; - private static final double M1350_MASS_SPENT = 1.970; - - - private static final double BOOSTER_SET_NO_MOTORS_MASS = 0.4555128227852; - private static final double BOOSTER_SET_NO_MOTORS_CMX = 1.246297525; - private static final double BOOSTER_SET_SPENT_MASS = BOOSTER_SET_NO_MOTORS_MASS + G77_MASS_SPENT*8; - - - @Test - public void testRocketNoMotors() { - Rocket rkt = TestRockets.makeNoMotorRocket(); - FlightConfiguration config = rkt.getEmptyConfiguration(); + public void testAlphaIIIStructure() { + Rocket rocket = TestRockets.makeEstesAlphaIII(); + rocket.setName("AlphaIII."+Thread.currentThread().getStackTrace()[1].getMethodName()); + + FlightConfiguration config = rocket.getEmptyConfiguration(); config.setAllStages(); - rkt.setName("TestRocket."+Thread.currentThread().getStackTrace()[1].getMethodName()); - // Validate Boosters - MassCalculator mc = new MassCalculator(); - // any config will do, beceause the rocket literally has no defined motors. - Coordinate rocketCM = mc.getRocketSpentMassData( config).getCM( ); + final RigidBody actualStructure = MassCalculator.calculateStructure( config ); + final double actualRocketDryMass = actualStructure.cm.weight; + final Coordinate actualRocketDryCM = actualStructure.cm; - double expMass = 0.668984592; - double expCMx = 0.558422219894; - double calcMass = rocketCM.weight; - Coordinate expCM = new Coordinate(expCMx,0,0, expMass); - assertEquals("Simple Motor Rocket mass incorrect: ", expMass, calcMass, EPSILON); - assertEquals("Simple Rocket CM.x is incorrect: ", expCM.x, rocketCM.x, EPSILON); - assertEquals("Simple Rocket CM.y is incorrect: ", expCM.y, rocketCM.y, EPSILON); - assertEquals("Simple Rocket CM.z is incorrect: ", expCM.z, rocketCM.z, EPSILON); - assertEquals("Simple Rocket CM is incorrect: ", expCM, rocketCM); + double expRocketDryMass = 0.025268247714878626; + assertEquals(" Alpha III Empty Mass is incorrect: ", expRocketDryMass, actualRocketDryMass, EPSILON); - rocketCM = mc.getRocketLaunchMassData(config).getCM( ); - assertEquals("Simple Rocket w/no Motors: CM is incorrect: ", expCM, rocketCM); + double expCMx = 0.1917685523; + Coordinate expCM = new Coordinate(expCMx,0,0, expRocketDryMass); + assertEquals("Simple Rocket CM.x is incorrect: ", expCM.x, actualRocketDryCM.x, EPSILON); + assertEquals("Simple Rocket CM.y is incorrect: ", expCM.y, actualRocketDryCM.y, EPSILON); + assertEquals("Simple Rocket CM.z is incorrect: ", expCM.z, actualRocketDryCM.z, EPSILON); + assertEquals("Simple Rocket CM is incorrect: ", expCM, actualRocketDryCM); } @Test - public void testComponentMasses() { + public void testAlphaIIILaunchMass() { + Rocket rocket = TestRockets.makeEstesAlphaIII(); + rocket.setName("AlphaIII."+Thread.currentThread().getStackTrace()[1].getMethodName()); + + FlightConfiguration config = rocket.getFlightConfigurationByIndex(0, false); + + InnerTube mmt = (InnerTube)rocket.getChild(0).getChild(1).getChild(2); + Motor activeMotor = mmt.getMotorConfig( config.getFlightConfigurationID() ).getMotor(); + String desig = activeMotor.getDesignation(); + + RigidBody actualLaunchRigidBody = MassCalculator.calculateLaunch( config ); + double actualRocketLaunchMass = actualLaunchRigidBody.getMass(); + final Coordinate actualRocketLaunchCM = actualLaunchRigidBody.cm; + + double expRocketLaunchMass = 0.041668247714878634; + assertEquals(" Alpha III Total Mass (with motor: "+desig+") is incorrect: ", expRocketLaunchMass, actualRocketLaunchMass, EPSILON); + + double expCMx = 0.20996455968266833; + Coordinate expCM = new Coordinate(expCMx,0,0, expRocketLaunchMass); + assertEquals("Simple Rocket CM.x is incorrect: ", expCM.x, actualRocketLaunchCM.x, EPSILON); + assertEquals("Simple Rocket CM.y is incorrect: ", expCM.y, actualRocketLaunchCM.y, EPSILON); + assertEquals("Simple Rocket CM.z is incorrect: ", expCM.z, actualRocketLaunchCM.z, EPSILON); + assertEquals("Simple Rocket CM is incorrect: ", expCM, actualRocketLaunchCM); + } + + @Test + public void testAlphaIIIMotorMass() { + Rocket rocket = TestRockets.makeEstesAlphaIII(); + rocket.setName("AlphaIII."+Thread.currentThread().getStackTrace()[1].getMethodName()); + + InnerTube mmt = (InnerTube) rocket.getChild(0).getChild(1).getChild(2); + FlightConfiguration config = rocket.getFlightConfigurationByIndex(0, false); + FlightConfigurationId fcid = config.getFlightConfigurationID(); + Motor activeMotor = mmt.getMotorConfig( fcid ).getMotor(); + String desig = activeMotor.getDesignation(); + + final double expMotorLaunchMass = activeMotor.getLaunchMass(); // 0.0164 kg + + RigidBody actualMotorData = MassCalculator.calculateMotor( config ); + + assertEquals(" Motor Mass "+desig+" is incorrect: ", expMotorLaunchMass, actualMotorData.getMass(), EPSILON); + + double expCMx = 0.238; + Coordinate expCM = new Coordinate(expCMx,0,0, expMotorLaunchMass); + assertEquals("Simple Rocket CM.x is incorrect: ", expCM.x, actualMotorData.cm.x, EPSILON); + assertEquals("Simple Rocket CM.y is incorrect: ", expCM.y, actualMotorData.cm.y, EPSILON); + assertEquals("Simple Rocket CM.z is incorrect: ", expCM.z, actualMotorData.cm.z, EPSILON); + assertEquals("Simple Rocket CM is incorrect: ", expCM, actualMotorData.cm); + } + + + @Test + public void testAlphaIIIMotorSimulationMass() { + Rocket rocket = TestRockets.makeEstesAlphaIII(); + rocket.setName("AlphaIII."+Thread.currentThread().getStackTrace()[1].getMethodName()); + + InnerTube mmt = (InnerTube) rocket.getChild(0).getChild(1).getChild(2); + FlightConfiguration config = rocket.getFlightConfigurationByIndex(0, false); + FlightConfigurationId fcid = config.getFlightConfigurationID(); + Motor activeMotor = mmt.getMotorConfig( fcid ).getMotor(); + String desig = activeMotor.getDesignation(); + + // this is probably not enough for a full-up simulation, but it IS enough for a motor-mass calculation. + SimulationStatus status = new SimulationStatus( config, new SimulationConditions()); + + { + final double simTime = 0.03; // almost launch + status.setSimulationTime( simTime ); + RigidBody actualMotorData = MassCalculator.calculateMotor( status ); + double expMass = activeMotor.getTotalMass(simTime); + assertEquals(" Motor Mass "+desig+" is incorrect: ", expMass, actualMotorData.getMass(), EPSILON); + }{ + final double simTime = 1.03; // middle + status.setSimulationTime( simTime ); + RigidBody actualMotorData = MassCalculator.calculateMotor( status ); + double expMass = activeMotor.getTotalMass(simTime); + assertEquals(" Motor Mass "+desig+" is incorrect: ", expMass, actualMotorData.getMass(), EPSILON); + }{ + final double simTime = 2.03; // after burnout + status.setSimulationTime( simTime ); + RigidBody actualMotorData = MassCalculator.calculateMotor( status ); + double expMass = activeMotor.getTotalMass(simTime); + assertEquals(" Motor Mass "+desig+" is incorrect: ", expMass, actualMotorData.getMass(), EPSILON); + } + } + + @Test + public void testFalcon9HComponentMasses() { Rocket rkt = TestRockets.makeFalcon9Heavy(); - rkt.setName("TestRocket."+Thread.currentThread().getStackTrace()[1].getMethodName()); + rkt.setName("Falcon9Heavy."+Thread.currentThread().getStackTrace()[1].getMethodName()); double expMass; RocketComponent cc; @@ -101,7 +174,7 @@ public class MassCalculatorTest extends BaseTestCase { expMass = 0.0079759509252; cc= rkt.getChild(0).getChild(3).getChild(0); compMass = cc.getComponentMass(); - assertEquals(cc.getName()+" mass calculated incorrectly: ", expMass, compMass, EPSILON); + assertEquals(cc.getName()+" mass calculated incorrectly: ", expMass, compMass, EPSILON); expMass = 0.00072; cc= rkt.getChild(0).getChild(3).getChild(1); @@ -139,12 +212,12 @@ public class MassCalculatorTest extends BaseTestCase { NoseCone nose = (NoseCone) boosters.getChild(0); compMass = nose.getComponentMass(); assertEquals( nose.getName()+" mass calculated incorrectly: ", expMass, compMass, EPSILON); - + expMass = 0.129886006; BodyTube body = (BodyTube) boosters.getChild(1); compMass = body.getComponentMass(); assertEquals( body.getName()+" mass calculated incorrectly: ", expMass, compMass, EPSILON); - + expMass = 0.01890610458; InnerTube mmt = (InnerTube)boosters.getChild(1).getChild(0); compMass = mmt.getComponentMass(); @@ -153,10 +226,93 @@ public class MassCalculatorTest extends BaseTestCase { } @Test - public void testComponentMOIs() { + public void testFalcon9HComponentCM() { + Rocket rkt = TestRockets.makeFalcon9Heavy(); + rkt.setName("Falcon9Heavy."+Thread.currentThread().getStackTrace()[1].getMethodName()); + + double expCMx; + double actCMx; + // ====== Payload Stage ====== + // ====== ====== ====== ====== + { + expCMx= 0.080801726467; + NoseCone nc = (NoseCone)rkt.getChild(0).getChild(0); + actCMx = nc.getComponentCG().x; + assertEquals("P/L NoseCone CMx calculated incorrectly: ", expCMx, actCMx, EPSILON); + + expCMx = 0.066; + BodyTube plbody = (BodyTube)rkt.getChild(0).getChild(1); + actCMx = plbody.getComponentCG().x; + assertEquals("P/L Body CMx calculated incorrectly: ", expCMx, actCMx, EPSILON); + + expCMx = 0.006640945419; + Transition tr= (Transition)rkt.getChild(0).getChild(2); + actCMx = tr.getComponentCG().x; + assertEquals("P/L Transition CMx calculated incorrectly: ", expCMx, actCMx, EPSILON); + + expCMx = 0.09; + BodyTube upperBody = (BodyTube)rkt.getChild(0).getChild(3); + actCMx = upperBody.getComponentCG().x; + assertEquals("P/L Upper Stage Body CMx calculated incorrectly: ", expCMx, actCMx, EPSILON); + { + expCMx = 0.0125; + Parachute chute = (Parachute)rkt.getChild(0).getChild(3).getChild(0); + actCMx = chute.getComponentCG().x; + assertEquals("Parachute CMx calculated incorrectly: ", expCMx, actCMx, EPSILON); + + expCMx = 0.0125; + ShockCord cord= (ShockCord)rkt.getChild(0).getChild(3).getChild(1); + actCMx = cord.getComponentCG().x; + assertEquals("Shock Cord CMx calculated incorrectly: ", expCMx, actCMx, EPSILON); + } + + expCMx = 0.06; + BodyTube interstage = (BodyTube)rkt.getChild(0).getChild(4); + actCMx = interstage.getComponentCG().x; + assertEquals("Interstage CMx calculated incorrectly: ", expCMx, actCMx, EPSILON); + } + + // ====== Core Stage ====== + // ====== ====== ====== + { + expCMx = 0.4; + BodyTube coreBody = (BodyTube)rkt.getChild(1).getChild(0); + actCMx = coreBody.getComponentCG().x; + assertEquals("Core Body CMx calculated incorrectly: ", expCMx, actCMx, EPSILON); + + expCMx = 0.19393939; + FinSet coreFins = (FinSet)rkt.getChild(1).getChild(0).getChild(0); + actCMx = coreFins .getComponentCG().x; + assertEquals("Core Fins CMx calculated incorrectly: ", expCMx, actCMx, EPSILON); + } + + // ====== Booster Set Stage ====== + // ====== ====== ====== + ParallelStage boosters = (ParallelStage) rkt.getChild(1).getChild(1); + { + expCMx = 0.055710581052; + // think of the casts as an assert that ( child instanceof NoseCone) == true + NoseCone nose = (NoseCone) boosters.getChild(0); + actCMx = nose.getComponentCG().x; + assertEquals("Booster Nose CMx calculated incorrectly: ", expCMx, actCMx, EPSILON); + + expCMx = 0.4; + BodyTube body = (BodyTube) boosters.getChild(1); + actCMx = body.getComponentCG().x; + assertEquals("BoosterBody CMx calculated incorrectly: ", expCMx, actCMx, EPSILON); + + expCMx = 0.075; + InnerTube mmt = (InnerTube)boosters.getChild(1).getChild(0); + actCMx = mmt.getComponentCG().x; + assertEquals(" Motor Mount Tube CMx calculated incorrectly: ", expCMx, actCMx, EPSILON); + } + } + + @Test + public void testFalcon9HComponentMOI() { Rocket rocket = TestRockets.makeFalcon9Heavy(); - rocket.setName("TestRocket."+Thread.currentThread().getStackTrace()[1].getMethodName()); - + rocket.setName("Falcon9Heavy."+Thread.currentThread().getStackTrace()[1].getMethodName()); + FlightConfiguration emptyConfig = rocket.getEmptyConfiguration(); rocket.setSelectedConfiguration( emptyConfig.getFlightConfigurationID() ); @@ -278,534 +434,385 @@ public class MassCalculatorTest extends BaseTestCase { } } - @Test - public void testPropellantMasses() { + public void testFalcon9HPayloadStructureCM() { Rocket rocket = TestRockets.makeFalcon9Heavy(); - rocket.setName("TestRocket."+Thread.currentThread().getStackTrace()[1].getMethodName()); - - FlightConfiguration config = rocket.getFlightConfiguration( new FlightConfigurationId( TestRockets.FALCON_9_FCID_1) ); - config.setAllStages(); - - MassCalculator calc = new MassCalculator(); - { // test core stage motors - AxialStage core = (AxialStage) rocket.getChild(1); - final int coreNum = core.getStageNumber(); - config.setOnlyStage( coreNum); - - MassData corePropInertia = calc.calculatePropellantMassData(config); - final Coordinate actCM= corePropInertia.getCM(); - final double actCorePropMass = corePropInertia.getMass(); - final MotorMount mnt = (MotorMount)core.getChild(0); - final Motor coreMotor = mnt.getMotorConfig( config.getFlightConfigurationID()).getMotor(); - - final double expCorePropMassEach = M1350_MASS_LAUNCH - M1350_MASS_SPENT; - final double coreMotorCount = 1.; - final double expCorePropMass = expCorePropMassEach * coreMotorCount; - - final Coordinate expCM = new Coordinate( 1.053, 0, 0, expCorePropMass); - - assertEquals(core.getName()+" => "+coreMotor.getDesignation()+" propellant mass is incorrect: ", expCorePropMass, actCorePropMass, EPSILON); - assertEquals(core.getName()+" => "+coreMotor.getDesignation()+" propellant CoM x is incorrect: ", expCM.x, actCM.x, EPSILON); - assertEquals(core.getName()+" => "+coreMotor.getDesignation()+" propellant CoM y is incorrect: ", expCM.y, actCM.y, EPSILON); - assertEquals(core.getName()+" => "+coreMotor.getDesignation()+" propellant CoM z is incorrect: ", expCM.z, actCM.z, EPSILON); - - - } - - { // test booster stage motors - ParallelStage boosters = (ParallelStage) rocket.getChild(1).getChild(1); - final int boostNum = boosters.getStageNumber(); - config.setOnlyStage( boostNum); - - MassData boosterPropInertia = calc.calculatePropellantMassData(config); - final Coordinate actCM= boosterPropInertia.getCM(); - final double actBoosterPropMass = boosterPropInertia.getMass(); - final MotorMount mnt = (MotorMount)boosters.getChild(1).getChild(0); - final Motor boosterMotor = mnt.getMotorConfig( config.getFlightConfigurationID()).getMotor(); - - final double expBoosterPropMassEach = G77_MASS_LAUNCH - G77_MASS_SPENT; - final double boosterSetMotorCount = 8.; /// it's a double merely to prevent type-casting issues - final double expBoosterPropMass = expBoosterPropMassEach * boosterSetMotorCount; - - final Coordinate expCM = new Coordinate( 1.31434, 0, 0, expBoosterPropMass); - - assertEquals( boosters.getName()+" => "+boosterMotor.getDesignation()+" propellant mass is incorrect: ", expBoosterPropMass, actBoosterPropMass, EPSILON); - assertEquals( boosters.getName()+" => "+boosterMotor.getDesignation()+" propellant CoM x is incorrect: ", expCM.x, actCM.x, EPSILON); - assertEquals( boosters.getName()+" => "+boosterMotor.getDesignation()+" propellant CoM y is incorrect: ", expCM.y, actCM.y, EPSILON); - assertEquals( boosters.getName()+" => "+boosterMotor.getDesignation()+" propellant CoM z is incorrect: ", expCM.z, actCM.z, EPSILON); - } - - - } - - @Test - public void testPropellantMOIs() { - Rocket rocket = TestRockets.makeFalcon9Heavy(); - rocket.setName("TestRocket."+Thread.currentThread().getStackTrace()[1].getMethodName()); - - FlightConfiguration config = rocket.getFlightConfiguration( new FlightConfigurationId( TestRockets.FALCON_9_FCID_1) ); - - { // test core stage motors - AxialStage core = (AxialStage) rocket.getChild(1); - final int coreNum = core.getStageNumber(); - config.setOnlyStage( coreNum); - - MassCalculator calc = new MassCalculator(); - MassData corePropInertia = calc.calculatePropellantMassData(config); - final double actCorePropMass = corePropInertia.getMass(); - final MotorMount mnt = (MotorMount)core.getChild(0); - final Motor coreMotor = mnt.getMotorConfig( config.getFlightConfigurationID()).getMotor(); - - // validated against a specific motor/radius/length - final double expIxxEach = 0.00199546875; - final double expIyyEach = 0.092495800375; - - final double actIxxEach = coreMotor.getUnitIxx()*actCorePropMass; - final double actIyyEach = coreMotor.getUnitIyy()*actCorePropMass; - final double coreMotorCount = mnt.getInstanceCount(); - final double actCorePropIxx = actIxxEach*coreMotorCount; - final double actCorePropIyy = actIyyEach*coreMotorCount; - - assertEquals(core.getName()+" propellant axial MOI is incorrect: ", expIxxEach, actCorePropIxx, EPSILON); - assertEquals(core.getName()+" propellant longitudinal MOI is incorrect: ", expIyyEach, actCorePropIyy, EPSILON); - } - - { // test booster stage motors - ParallelStage boosters = (ParallelStage) rocket.getChild(1).getChild(1); - final int boostNum = boosters.getStageNumber(); - config.setOnlyStage( boostNum); - - MassCalculator calc = new MassCalculator(); - MassData boosterPropInertia = calc.calculatePropellantMassData(config); - final double actBoosterPropMass = boosterPropInertia.getMass(); - final MotorMount mnt = (MotorMount)boosters.getChild(1).getChild(0); - final Motor boosterMotor = mnt.getMotorConfig( config.getFlightConfigurationID()).getMotor(); - - final double expBrIxxEach = 3.96952E-4; - final double expBrIyyEach = 0.005036790; - - final double actIxxEach = boosterMotor.getUnitIxx()*actBoosterPropMass; - final double actIyyEach = boosterMotor.getUnitIyy()*actBoosterPropMass; - final int boosterMotorCount = mnt.getInstanceCount(); - assertThat( boosters.getName()+" booster motor count is not as expected! ", boosterMotorCount, equalTo(8)); - final double actBoosterPropIxx = actIxxEach*boosterMotorCount; - final double actBoosterPropIyy = actIyyEach*boosterMotorCount; - - assertEquals(boosters.getName()+" propellant axial MOI is incorrect: ", expBrIxxEach, actBoosterPropIxx, EPSILON); - assertEquals(boosters.getName()+" propellant longitudinal MOI is incorrect: ", expBrIyyEach, actBoosterPropIyy, EPSILON); - } - - } - - @Test - public void testBoosterStructureCM() { - Rocket rocket = TestRockets.makeFalcon9Heavy(); - rocket.setName("TestRocket."+Thread.currentThread().getStackTrace()[1].getMethodName()); + rocket.setName("Falcon9Heavy."+Thread.currentThread().getStackTrace()[1].getMethodName()); FlightConfiguration config = rocket.getEmptyConfiguration(); - MassCalculator mc = new MassCalculator(); - { - // validate payload stage - AxialStage payloadStage = (AxialStage) rocket.getChild(0); - int plNum = payloadStage.getStageNumber(); - config.setOnlyStage( plNum ); - -// System.err.println( config.toStageListDetail()); -// System.err.println( rocket.toDebugTree()); - - MassData upperMass = mc.calculateBurnoutMassData( config ); - Coordinate actCM = upperMass.getCM(); - - double expMass = 0.116287; - double expCMx = 0.278070785749; - assertEquals("Upper Stage Mass is incorrect: ", expMass, upperMass.getCM().weight, EPSILON); - - assertEquals("Upper Stage CM.x is incorrect: ", expCMx, upperMass.getCM().x, EPSILON); - assertEquals("Upper Stage CM.y is incorrect: ", 0.0f, upperMass.getCM().y, EPSILON); - assertEquals("Upper Stage CM.z is incorrect: ", 0.0f, upperMass.getCM().z, EPSILON); - } - { - // Validate Boosters - ParallelStage boosters = (ParallelStage) rocket.getChild(1).getChild(1); - int boostNum = boosters.getStageNumber(); - config.setOnlyStage( boostNum ); - - //System.err.println( config.toStageListDetail()); - //System.err.println( rocket.toDebugTree()); + // validate payload stage + AxialStage payloadStage = (AxialStage) rocket.getChild(0); + config.setOnlyStage( payloadStage.getStageNumber() ); - 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); - } - } + final RigidBody actualStructureData = MassCalculator.calculateStructure( config ); + final Coordinate actualCM = actualStructureData.cm; - @Test - public void testCMCache() { - Rocket rocket = TestRockets.makeFalcon9Heavy(); - rocket.setName("TestRocket."+Thread.currentThread().getStackTrace()[1].getMethodName()); + double expMass = 0.116287; + double expCMx = 0.278070785749; + assertEquals("Upper Stage Mass is incorrect: ", expMass, actualCM.weight, EPSILON); - 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 ); - - mc.voidMassCache(); - 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); - } + assertEquals("Upper Stage CM.x is incorrect: ", expCMx, actualCM.x, EPSILON); + assertEquals("Upper Stage CM.y is incorrect: ", 0.0f, actualCM.y, EPSILON); + assertEquals("Upper Stage CM.z is incorrect: ", 0.0f, actualCM.z, EPSILON); } - @Test - public void testSingleMotorMass() { - Rocket rocket = TestRockets.makeEstesAlphaIII(); + public void testFalcon9HCoreStructureCM() { + Rocket rocket = TestRockets.makeFalcon9Heavy(); + rocket.setName("Falcon9Heavy."+Thread.currentThread().getStackTrace()[1].getMethodName()); - InnerTube mmt = (InnerTube) rocket.getChild(0).getChild(1).getChild(2); - FlightConfiguration config = rocket.getFlightConfigurationByIndex(0, false); - FlightConfigurationId fcid = config.getFlightConfigurationID(); - Motor activeMotor = mmt.getMotorConfig( fcid ).getMotor(); - String desig = activeMotor.getDesignation(); + FlightConfiguration config = rocket.getEmptyConfiguration(); + AxialStage coreStage = (AxialStage) rocket.getChild(1); + config.setOnlyStage( coreStage.getStageNumber() ); - double expLaunchMass = 0.0164; // kg - double expSpentMass = 0.0131; // kg - assertEquals(" Motor Mass "+desig+" is incorrect: ", expLaunchMass, activeMotor.getLaunchMass(), EPSILON); - assertEquals(" Motor Mass "+desig+" is incorrect: ", expSpentMass, activeMotor.getBurnoutMass(), EPSILON); + final RigidBody actualData = MassCalculator.calculateStructure( config ); + final Coordinate actualCM = actualData.cm; + + double expMass = 0.343156; + double expCMx = 1.134252; + assertEquals("Upper Stage Mass is incorrect: ", expMass, actualCM.weight, EPSILON); + + assertEquals("Upper Stage CM.x is incorrect: ", expCMx, actualCM.x, EPSILON); + assertEquals("Upper Stage CM.y is incorrect: ", 0.0f, actualCM.y, EPSILON); + assertEquals("Upper Stage CM.z is incorrect: ", 0.0f, actualCM.z, EPSILON); + } + + @Test + public void testFalcon9HCoreMotorLaunchCM() { + Rocket rocket = TestRockets.makeFalcon9Heavy(); + rocket.setName("Falcon9Heavy."+Thread.currentThread().getStackTrace()[1].getMethodName()); + + FlightConfiguration config = rocket.getFlightConfiguration( new FlightConfigurationId( TestRockets.FALCON_9H_FCID_1) ); + AxialStage core = (AxialStage) rocket.getChild(1); + final int coreNum = core.getStageNumber(); + config.setOnlyStage( coreNum); + + final MotorMount mnt = (MotorMount)core.getChild(0); + final Motor motor = mnt.getMotorConfig( config.getFlightConfigurationID()).getMotor(); + final String motorDesignation= motor.getDesignation(); + + RigidBody actMotorData = MassCalculator.calculateMotor( config ); + + final double actMotorMass = actMotorData.getMass(); + final Coordinate actCM= actMotorData.cm; + + final double expMotorMass = motor.getLaunchMass(); + final Coordinate expCM = new Coordinate( 1.053, 0, 0, expMotorMass); + + assertEquals(core.getName()+" => "+motorDesignation+" propellant mass is incorrect: ", expMotorMass, actMotorMass, EPSILON); + assertEquals(core.getName()+" => "+motorDesignation+" propellant CoM x is incorrect: ", expCM.x, actCM.x, EPSILON); + assertEquals(core.getName()+" => "+motorDesignation+" propellant CoM y is incorrect: ", expCM.y, actCM.y, EPSILON); + assertEquals(core.getName()+" => "+motorDesignation+" propellant CoM z is incorrect: ", expCM.z, actCM.z, EPSILON); + } + + @Test + public void testFalcon9HCoreMotorLaunchMOIs() { + Rocket rocket = TestRockets.makeFalcon9Heavy(); + rocket.setName("Falcon9Heavy."+Thread.currentThread().getStackTrace()[1].getMethodName()); + + FlightConfiguration config = rocket.getFlightConfiguration( new FlightConfigurationId( TestRockets.FALCON_9H_FCID_1) ); + config.setOnlyStage( 1 ); + + RigidBody corePropInertia = MassCalculator.calculateMotor( config ); + + // validated against a specific motor/radius/length + final double expIxx = 0.003380625; + + final double expIyy = 0.156701835; + + final double actCorePropIxx = corePropInertia.getIxx(); + final double actCorePropIyy = corePropInertia.getIyy(); + + assertEquals("Core Stage motor axial MOI is incorrect: ", expIxx, actCorePropIxx, EPSILON); + assertEquals("Core Stage motor longitudinal MOI is incorrect: ", expIyy, actCorePropIyy, EPSILON); + } + + @Test + public void testFalcon9HBoosterStructureCM() { + Rocket rocket = TestRockets.makeFalcon9Heavy(); + rocket.setName("Falcon9Heavy."+Thread.currentThread().getStackTrace()[1].getMethodName()); + + FlightConfiguration config = rocket.getEmptyConfiguration(); + + ParallelStage boosters = (ParallelStage) rocket.getChild(1).getChild(1); + config.setOnlyStage( boosters.getStageNumber() ); + + final RigidBody actualData = MassCalculator.calculateStructure( config ); + final Coordinate actualCM = actualData.getCM(); + + double expMass = 0.34207619524942634; + double expCMx = 0.9447396557660297; + assertEquals("Heavy Booster Mass is incorrect: ", expMass, actualCM.weight, EPSILON); + + assertEquals("Heavy Booster CM.x is incorrect: ", expCMx, actualCM.x, EPSILON); + assertEquals("Heavy Booster CM.y is incorrect: ", 0.0f, actualCM.y, EPSILON); + assertEquals("Heavy Booster CM.z is incorrect: ", 0.0f, actualCM.z, EPSILON); + } + + @Test + public void testFalcon9HBoosterLaunchCM() { + Rocket rocket = TestRockets.makeFalcon9Heavy(); + rocket.setName("Falcon9Heavy."+Thread.currentThread().getStackTrace()[1].getMethodName()); + + FlightConfiguration config = rocket.getFlightConfiguration( new FlightConfigurationId( TestRockets.FALCON_9H_FCID_1)); + config.setOnlyStage( TestRockets.FALCON_9H_BOOSTER_STAGE_NUMBER ); + + RigidBody actualBoosterLaunchData = MassCalculator.calculateLaunch( config ); + + double actualMass = actualBoosterLaunchData.getMass(); + double expectedMass = 1.3260761952; + assertEquals(" Booster Launch Mass is incorrect: ", expectedMass, actualMass, EPSILON); + + final Coordinate actualCM = actualBoosterLaunchData.getCM(); + double expectedCMx = 1.21899745; + Coordinate expCM = new Coordinate(expectedCMx,0,0, expectedMass); + assertEquals(" Booster Launch CM.x is incorrect: ", expCM.x, actualCM.x, EPSILON); + assertEquals(" Booster Launch CM.y is incorrect: ", expCM.y, actualCM.y, EPSILON); + assertEquals(" Booster Launch CM.z is incorrect: ", expCM.z, actualCM.z, EPSILON); + assertEquals(" Booster Launch CM is incorrect: ", expCM, actualCM); + } + + @Test + public void testFalcon9HBoosterSpentCM(){ + Rocket rocket = TestRockets.makeFalcon9Heavy(); + rocket.setName("Falcon9Heavy."+Thread.currentThread().getStackTrace()[1].getMethodName()); + + FlightConfiguration config = rocket.getFlightConfiguration( new FlightConfigurationId( TestRockets.FALCON_9H_FCID_1)); + config.setOnlyStage( TestRockets.FALCON_9H_BOOSTER_STAGE_NUMBER ); // Validate Booster Launch Mass - MassCalculator mc = new MassCalculator(); - MassData propMassData = mc.calculatePropellantMassData( config); - double actPropMass = propMassData.getCM().weight; + RigidBody spentData = MassCalculator.calculateBurnout( config ); + Coordinate spentCM = spentData.getCM(); - double expPropMass = expLaunchMass - expSpentMass; - assertEquals(" Motor Mass "+desig+" is incorrect: ", expPropMass, actPropMass, EPSILON); + double expSpentMass = 0.8540761952494624; + double expSpentCMx = 1.166306978799226; + Coordinate expLaunchCM = new Coordinate( expSpentCMx, 0, 0, expSpentMass); + assertEquals(" Booster Launch Mass is incorrect: ", expLaunchCM.weight, spentCM.weight, EPSILON); + assertEquals(" Booster Launch CM.x is incorrect: ", expLaunchCM.x, spentCM.x, EPSILON); + assertEquals(" Booster Launch CM.y is incorrect: ", expLaunchCM.y, spentCM.y, EPSILON); + assertEquals(" Booster Launch CM.z is incorrect: ", expLaunchCM.z, spentCM.z, EPSILON); + assertEquals(" Booster Launch CM is incorrect: ", expLaunchCM, spentCM); } @Test - public void testBoosterPropellantInertia() { + public void testFalcon9HBoosterMotorCM() { Rocket rocket = TestRockets.makeFalcon9Heavy(); - ParallelStage boosters = (ParallelStage) rocket.getChild(1).getChild(1); - int boostNum = boosters.getStageNumber(); - FlightConfiguration config = rocket.getFlightConfiguration( new FlightConfigurationId( TestRockets.FALCON_9_FCID_1)); - config.setOnlyStage( boostNum); + rocket.setName("Falcon9Heavy."+Thread.currentThread().getStackTrace()[1].getMethodName()); - InnerTube mmt = (InnerTube) boosters.getChild(1).getChild(0); - { - double expX = (.564 + 0.8 - 0.150 ); - double actX = mmt.getLocations()[0].x; - assertEquals(" Booster motor mount tubes located incorrectly: ", expX, actX, EPSILON); - } - { - // Validate Booster Propellant Mass - Motor mtr = mmt.getMotorConfig( config.getId()).getMotor(); - double propMassEach = mtr.getLaunchMass()-mtr.getBurnoutMass(); - MassCalculator mc = new MassCalculator(); - MassData propMotorData = mc.calculatePropellantMassData( config ); - Coordinate propCM = propMotorData.getCM(); - Coordinate expPropCM = new Coordinate(1.31434, 0, 0, propMassEach*2*4); - assertEquals(" Booster Prop Mass is incorrect: ", expPropCM.weight, propCM.weight, EPSILON); - assertEquals(" Booster Prop CM.x is incorrect: ", expPropCM.x, propCM.x, EPSILON); - assertEquals(" Booster Prop CM.y is incorrect: ", expPropCM.y, propCM.y, EPSILON); - assertEquals(" Booster Prop CM.z is incorrect: ", expPropCM.z, propCM.z, EPSILON); - assertEquals(" Booster Prop CM is incorrect: ", expPropCM, propCM); - } - } + FlightConfiguration config = rocket.getFlightConfiguration( new FlightConfigurationId( TestRockets.FALCON_9H_FCID_1) ); + config.setOnlyStage( TestRockets.FALCON_9H_BOOSTER_STAGE_NUMBER ); + + RigidBody actualPropellant = MassCalculator.calculateMotor( config ); + final Coordinate actCM= actualPropellant.getCM(); + + ParallelStage boosters = (ParallelStage) rocket.getChild(1).getChild(1); + final MotorMount mnt = (MotorMount)boosters.getChild(1).getChild(0); + final Motor boosterMotor = mnt.getMotorConfig( config.getFlightConfigurationID()).getMotor(); + + final double expBoosterPropMassEach = boosterMotor.getLaunchMass(); + final double boosterSetMotorCount = 8.; /// use a double merely to prevent type-casting issues + final double expBoosterPropMass = expBoosterPropMassEach * boosterSetMotorCount; + + final Coordinate expCM = new Coordinate( 1.31434, 0, 0, expBoosterPropMass); + + assertEquals( boosters.getName()+" => "+boosterMotor.getDesignation()+" propellant mass is incorrect: ", expBoosterPropMass, actualPropellant.getMass(), EPSILON); + assertEquals( boosters.getName()+" => "+boosterMotor.getDesignation()+" propellant CoM x is incorrect: ", expCM.x, actCM.x, EPSILON); + assertEquals( boosters.getName()+" => "+boosterMotor.getDesignation()+" propellant CoM y is incorrect: ", expCM.y, actCM.y, EPSILON); + assertEquals( boosters.getName()+" => "+boosterMotor.getDesignation()+" propellant CoM z is incorrect: ", expCM.z, actCM.z, EPSILON); + } + + @Test + public void testFalcon9HeavyBoosterMotorLaunchMOIs() { + Rocket rocket = TestRockets.makeFalcon9Heavy(); + rocket.setName("Falcon9Heavy."+Thread.currentThread().getStackTrace()[1].getMethodName()); + + FlightConfiguration config = rocket.getFlightConfiguration( new FlightConfigurationId( TestRockets.FALCON_9H_FCID_1) ); + config.setOnlyStage( TestRockets.FALCON_9H_BOOSTER_STAGE_NUMBER ); + + // System.err.println( rocket.toDebugTree()); + + RigidBody actualInertia = MassCalculator.calculateMotor( config ); + + final double expIxx = 0.006081243; - @Test - public void testBoosterSpentCM(){ - Rocket rocket = TestRockets.makeFalcon9Heavy(); - ParallelStage boosters = (ParallelStage) rocket.getChild(1).getChild(1); - int boostNum = boosters.getStageNumber(); - FlightConfiguration config = rocket.getFlightConfiguration( new FlightConfigurationId( TestRockets.FALCON_9_FCID_1)); - config.setOnlyStage( boostNum); - - { - // Validate Booster Launch Mass - MassCalculator mc = new MassCalculator(); - MassData launchData = mc.calculateBurnoutMassData( config ); - Coordinate launchCM = launchData.getCM(); - double expLaunchCMx = 1.2823050552779347; - double expLaunchMass = BOOSTER_SET_SPENT_MASS; - Coordinate expLaunchCM = new Coordinate( expLaunchCMx, 0, 0, expLaunchMass); - assertEquals(" Booster Launch Mass is incorrect: ", expLaunchCM.weight, launchCM.weight, EPSILON); - assertEquals(" Booster Launch CM.x is incorrect: ", expLaunchCM.x, launchCM.x, EPSILON); - assertEquals(" Booster Launch CM.y is incorrect: ", expLaunchCM.y, launchCM.y, EPSILON); - assertEquals(" Booster Launch CM.z is incorrect: ", expLaunchCM.z, launchCM.z, EPSILON); - assertEquals(" Booster Launch CM is incorrect: ", expLaunchCM, launchCM); - } - } - - - @Test - public void testBoosterLaunchCM() { - Rocket rocket = TestRockets.makeFalcon9Heavy(); - ParallelStage boosters = (ParallelStage) rocket.getChild(1).getChild(1); - int boostNum = boosters.getStageNumber(); - FlightConfiguration config = rocket.getFlightConfiguration( new FlightConfigurationId( TestRockets.FALCON_9_FCID_1)); - config.setOnlyStage( boostNum); - - { - // Validate Booster Launch Mass - MassCalculator mc = new MassCalculator(); - Coordinate boosterSetLaunchCM = mc.getRocketLaunchMassData( rocket.getSelectedConfiguration()).getCM(); - double calcTotalMass = boosterSetLaunchCM.weight; - - double expTotalMass = BOOSTER_SET_NO_MOTORS_MASS + 2*4*G77_MASS_LAUNCH; - assertEquals(" Booster Launch Mass is incorrect: ", expTotalMass, calcTotalMass, EPSILON); - - double expX = 1.292808951; - Coordinate expCM = new Coordinate(expX,0,0, expTotalMass); - assertEquals(" Booster Launch CM.x is incorrect: ", expCM.x, boosterSetLaunchCM.x, EPSILON); - assertEquals(" Booster Launch CM.y is incorrect: ", expCM.y, boosterSetLaunchCM.y, EPSILON); - assertEquals(" Booster Launch CM.z is incorrect: ", expCM.z, boosterSetLaunchCM.z, EPSILON); - assertEquals(" Booster Launch CM is incorrect: ", expCM, boosterSetLaunchCM); - } + final double expIyy = 0.001312553; + assertEquals("Booster stage propellant axial MOI is incorrect: ", expIxx, actualInertia.getIxx(), EPSILON); + assertEquals("Booster stage propellant longitudinal MOI is incorrect: ", expIyy, actualInertia.getIyy(), EPSILON); } @Test - public void testBoosterSpentMOIs() { + public void testFalcon9HeavyBoosterSpentMOIs() { Rocket rocket = TestRockets.makeFalcon9Heavy(); - rocket.setName("TestRocket."+Thread.currentThread().getStackTrace()[1].getMethodName()); - FlightConfiguration config = rocket.getFlightConfiguration( new FlightConfigurationId( TestRockets.FALCON_9_FCID_1)); - ParallelStage boosters = (ParallelStage) rocket.getChild(1).getChild(1); - int boostNum = boosters.getStageNumber(); - config.setOnlyStage( boostNum); + rocket.setName("Falcon9Heavy."+Thread.currentThread().getStackTrace()[1].getMethodName()); - // Validate Boosters - MassCalculator mc = new MassCalculator(); + FlightConfiguration config = rocket.getFlightConfiguration( new FlightConfigurationId( TestRockets.FALCON_9H_FCID_1)); + config.setOnlyStage( TestRockets.FALCON_9H_BOOSTER_STAGE_NUMBER ); - MassData spent = mc.calculateBurnoutMassData( config); + RigidBody spent = MassCalculator.calculateBurnout( config); - double expMOIRotational = .0062065449; + double expMOIRotational = 0.005508340370; double boosterMOIRotational = spent.getRotationalInertia(); assertEquals(" Booster x-axis MOI is incorrect: ", expMOIRotational, boosterMOIRotational, EPSILON); - - double expMOI_tr = 0.13136525; + + double expMOI_tr = 0.054690069584; double boosterMOI_tr= spent.getLongitudinalInertia(); assertEquals(" Booster transverse MOI is incorrect: ", expMOI_tr, boosterMOI_tr, EPSILON); } @Test - public void testBoosterLaunchMOIs() { + public void testFalcon9HeavyBoosterLaunchMOIs() { Rocket rocket = TestRockets.makeFalcon9Heavy(); - rocket.setName("TestRocket:F9H:Total_MOI"); - FlightConfiguration config = rocket.getFlightConfiguration( new FlightConfigurationId( TestRockets.FALCON_9_FCID_1)); + rocket.setName("Falcon9Heavy."+Thread.currentThread().getStackTrace()[1].getMethodName()); - ParallelStage boosters = (ParallelStage) rocket.getChild(1).getChild(1); - int boostNum = boosters.getStageNumber(); - config.setOnlyStage( boostNum); + FlightConfiguration config = rocket.getFlightConfiguration( new FlightConfigurationId( TestRockets.FALCON_9H_FCID_1)); + config.setOnlyStage( TestRockets.FALCON_9H_BOOSTER_STAGE_NUMBER ); - // Validate Boosters - MassCalculator mc = new MassCalculator(); - - final MassData launch= mc.getRocketLaunchMassData( config); - final double expIxx = 0.00912327349; - final double actIxx= launch.getRotationalInertia(); - final double expIyy = 0.132320; - final double actIyy= launch.getLongitudinalInertia(); - + RigidBody launchData = MassCalculator.calculateLaunch( config); + + final double expIxx = 0.008425359370; + final double actIxx= launchData.getRotationalInertia(); + final double expIyy = 0.061981403261; + final double actIyy= launchData.getLongitudinalInertia(); + assertEquals(" Booster x-axis MOI is incorrect: ", expIxx, actIxx, EPSILON); assertEquals(" Booster transverse MOI is incorrect: ", expIyy, actIyy, EPSILON); } - + @Test - public void testStageMassOverride() { + public void testFalcon9HeavyBoosterStageMassOverride() { Rocket rocket = TestRockets.makeFalcon9Heavy(); - rocket.setName("TestRocket."+Thread.currentThread().getStackTrace()[1].getMethodName()); + rocket.setName("Falcon9Heavy."+Thread.currentThread().getStackTrace()[1].getMethodName()); + FlightConfiguration config = rocket.getEmptyConfiguration(); rocket.setSelectedConfiguration( config.getId() ); + config.setOnlyStage( TestRockets.FALCON_9H_BOOSTER_STAGE_NUMBER ); - ParallelStage boosters = (ParallelStage) rocket.getChild(1).getChild(1); - int boostNum = boosters.getStageNumber(); - config.setOnlyStage( boostNum); - - double overrideMass = 0.5; + final ParallelStage boosters = (ParallelStage) rocket.getChild(1).getChild(1); + final double overrideMass = 0.5; + boosters.setOverrideSubcomponents(true); boosters.setMassOverridden(true); boosters.setOverrideMass(overrideMass); - boosters.setCGOverridden(true); boosters.setOverrideCGX(6.0); - { - // Validate Mass - MassCalculator mc = new MassCalculator(); - - MassData burnout = mc.calculateBurnoutMassData( config); - Coordinate boosterSetCM = burnout.getCM(); - double calcTotalMass = boosterSetCM.weight; - - double expTotalMass = overrideMass; - assertEquals(" Booster Launch Mass is incorrect: ", expTotalMass, calcTotalMass, EPSILON); - - double expCMx = 6.0; - Coordinate expCM = new Coordinate( expCMx, 0, 0, expTotalMass); - assertEquals(" Booster Launch CM.x is incorrect: ", expCM.x, boosterSetCM.x, EPSILON); - assertEquals(" Booster Launch CM.y is incorrect: ", expCM.y, boosterSetCM.y, EPSILON); - assertEquals(" Booster Launch CM.z is incorrect: ", expCM.z, boosterSetCM.z, EPSILON); - assertEquals(" Booster Launch CM is incorrect: ", expCM, boosterSetCM); - // Validate MOI - double expMOI_axial = .00333912717; - double boosterMOI_xx= burnout.getRotationalInertia(); - assertEquals(" Booster x-axis MOI is incorrect: ", expMOI_axial, boosterMOI_xx, EPSILON); - - double expMOI_tr = 0.142220231; - double boosterMOI_tr= burnout.getLongitudinalInertia(); - assertEquals(" Booster transverse MOI is incorrect: ", expMOI_tr, boosterMOI_tr, EPSILON); - } + RigidBody burnout = MassCalculator.calculateStructure( config); + Coordinate boosterSetCM = burnout.getCM(); + double calcTotalMass = burnout.getMass(); + double expTotalMass = overrideMass; + assertEquals(" Booster Launch Mass is incorrect: ", expTotalMass, calcTotalMass, EPSILON); + + double expCMx = 6.0; + Coordinate expCM = new Coordinate( expCMx, 0, 0, expTotalMass); + assertEquals(" Booster Launch CM.x is incorrect: ", expCM.x, boosterSetCM.x, EPSILON); + assertEquals(" Booster Launch CM.y is incorrect: ", expCM.y, boosterSetCM.y, EPSILON); + assertEquals(" Booster Launch CM.z is incorrect: ", expCM.z, boosterSetCM.z, EPSILON); + assertEquals(" Booster Launch CM is incorrect: ", expCM, boosterSetCM); + + // Validate MOI + double expMOI_axial = 0.002344116370164005; + double boosterMOI_xx= burnout.getRotationalInertia(); + assertEquals(" Booster x-axis MOI is incorrect: ", expMOI_axial, boosterMOI_xx, EPSILON); + + double expMOI_tr = 8.885103994735; + double boosterMOI_tr= burnout.getLongitudinalInertia(); + assertEquals(" Booster transverse MOI is incorrect: ", expMOI_tr, boosterMOI_tr, EPSILON); } @Test - public void testComponentMassOverride() { + public void testFalcon9HeavyComponentMassOverride() { Rocket rocket = TestRockets.makeFalcon9Heavy(); - rocket.setName("TestRocket."+Thread.currentThread().getStackTrace()[1].getMethodName()); + rocket.setName("Falcon9Heavy."+Thread.currentThread().getStackTrace()[1].getMethodName()); FlightConfiguration config = rocket.getEmptyConfiguration(); rocket.setSelectedConfiguration( config.getId() ); ParallelStage boosters = (ParallelStage) rocket.getChild(1).getChild(1); - int boostNum = boosters.getStageNumber(); - config.setOnlyStage( boostNum); + config.setOnlyStage( boosters.getStageNumber() ); NoseCone nose = (NoseCone)boosters.getChild(0); nose.setMassOverridden(true); nose.setOverrideMass( 0.71 ); - + BodyTube body = (BodyTube)boosters.getChild(1); body.setMassOverridden(true); body.setOverrideMass( 0.622 ); - + InnerTube mmt = (InnerTube)boosters.getChild(1).getChild(0); mmt.setMassOverridden(true); mmt.setOverrideMass( 0.213 ); - { - // Validate Mass - MassCalculator mc = new MassCalculator(); - MassData burnout = mc.calculateBurnoutMassData( config); - Coordinate boosterSetCM = burnout.getCM(); - double calcTotalMass = boosterSetCM.weight; - - double expTotalMass = 4.368; - assertEquals(" Booster Launch Mass is incorrect: ", expTotalMass, calcTotalMass, EPSILON); - - double expCMx = 1.20642422735; - Coordinate expCM = new Coordinate( expCMx, 0, 0, expTotalMass); - assertEquals(" Booster Launch CM.x is incorrect: ", expCM.x, boosterSetCM.x, EPSILON); - assertEquals(" Booster Launch CM.y is incorrect: ", expCM.y, boosterSetCM.y, EPSILON); - assertEquals(" Booster Launch CM.z is incorrect: ", expCM.z, boosterSetCM.z, EPSILON); - assertEquals(" Booster Launch CM is incorrect: ", expCM, boosterSetCM); + RigidBody boosterData = MassCalculator.calculateStructure( config ); + Coordinate boosterCM = boosterData.getCM(); - // Validate MOI - double expMOI_axial = 0.0257485; - double boosterMOI_xx= burnout.getRotationalInertia(); - assertEquals(" Booster x-axis MOI is incorrect: ", expMOI_axial, boosterMOI_xx, EPSILON); - - double expMOI_tr = 1.633216231; - double boosterMOI_tr= burnout.getLongitudinalInertia(); - assertEquals(" Booster transverse MOI is incorrect: ", expMOI_tr, boosterMOI_tr, EPSILON); - } + double expTotalMass = 3.09; + assertEquals(" Booster Launch Mass is incorrect: ", expTotalMass, boosterData.getMass(), EPSILON); - } + double expCMx = 0.81382493; + Coordinate expCM = new Coordinate( expCMx, 0, 0, expTotalMass); + assertEquals(" Booster Launch CM.x is incorrect: ", expCM.x, boosterCM.x, EPSILON); + assertEquals(" Booster Launch CM.y is incorrect: ", expCM.y, boosterCM.y, EPSILON); + assertEquals(" Booster Launch CM.z is incorrect: ", expCM.z, boosterCM.z, EPSILON); + assertEquals(" Booster Launch CM is incorrect: ", expCM, boosterCM); + // Validate MOI + double expMOI_axial = 0.020436592808; + double boosterMOI_xx= boosterData.getRotationalInertia(); + assertEquals(" Booster x-axis MOI is incorrect: ", expMOI_axial, boosterMOI_xx, EPSILON); + + double expMOI_tr = 0.299042045787; + double boosterMOI_tr= boosterData.getLongitudinalInertia(); + assertEquals(" Booster transverse MOI is incorrect: ", expMOI_tr, boosterMOI_tr, EPSILON); + } + @Test - public void testCMOverride() { + public void testFalcon9HeavyComponentCMxOverride() { Rocket rocket = TestRockets.makeFalcon9Heavy(); - rocket.setName("TestRocket."+Thread.currentThread().getStackTrace()[1].getMethodName()); + rocket.setName("Falcon9Heavy."+Thread.currentThread().getStackTrace()[1].getMethodName()); + FlightConfiguration config = rocket.getEmptyConfiguration(); rocket.setSelectedConfiguration( config.getId() ); + config.setOnlyStage( TestRockets.FALCON_9H_BOOSTER_STAGE_NUMBER ); ParallelStage boosters = (ParallelStage) rocket.getChild(1).getChild(1); - int boostNum = boosters.getStageNumber(); - config.setOnlyStage( boostNum); NoseCone nose = (NoseCone)boosters.getChild(0); nose.setCGOverridden(true); nose.setOverrideCGX(0.22); - + BodyTube body = (BodyTube)boosters.getChild(1); body.setCGOverridden(true); body.setOverrideCGX( 0.433); - + InnerTube mmt = (InnerTube)boosters.getChild(1).getChild(0); mmt.setCGOverridden(true); mmt.setOverrideCGX( 0.395 ); - - { - // Validate Mass - MassCalculator mc = new MassCalculator(); - - MassData burnout = mc.calculateBurnoutMassData( config); - Coordinate boosterSetCM = burnout.getCM(); - - double expMass = BOOSTER_SET_NO_MOTORS_MASS; - double calcTotalMass = boosterSetCM.weight; - assertEquals(" Booster Launch Mass is incorrect: ", expMass, calcTotalMass, EPSILON); - - double expCMx = 1.38741685552577; - Coordinate expCM = new Coordinate( expCMx, 0, 0, expMass); - assertEquals(" Booster Launch CM.x is incorrect: ", expCM.x, boosterSetCM.x, EPSILON); - assertEquals(" Booster Launch CM.y is incorrect: ", expCM.y, boosterSetCM.y, EPSILON); - assertEquals(" Booster Launch CM.z is incorrect: ", expCM.z, boosterSetCM.z, EPSILON); - assertEquals(" Booster Launch CM is incorrect: ", expCM, boosterSetCM); - // Validate MOI - double expMOI_axial = 0.00304203; - double boosterMOI_xx= burnout.getRotationalInertia(); - assertEquals(" Booster x-axis MOI is incorrect: ", expMOI_axial, boosterMOI_xx, EPSILON); - - double expMOI_tr = 0.1893499746; - double boosterMOI_tr= burnout.getLongitudinalInertia(); - assertEquals(" Booster transverse MOI is incorrect: ", expMOI_tr, boosterMOI_tr, EPSILON); - } + RigidBody structure = MassCalculator.calculateStructure( config); + double expMass = 0.34207619524942634; + double calcTotalMass = structure.getMass(); + assertEquals(" Booster Launch Mass is incorrect: ", expMass, calcTotalMass, EPSILON); + + double expCMx = 1.0265399801199806; + Coordinate expCM = new Coordinate( expCMx, 0, 0, expMass); + assertEquals(" Booster Launch CM.x is incorrect: ", expCM.x, structure.getCM().x, EPSILON); + assertEquals(" Booster Launch CM.y is incorrect: ", expCM.y, structure.getCM().y, EPSILON); + assertEquals(" Booster Launch CM.z is incorrect: ", expCM.z, structure.getCM().z, EPSILON); + assertEquals(" Booster Launch CM is incorrect: ", expCM, structure.getCM()); + + // Validate MOI + double expMOI_axial = 0.002344116370; + double boosterMOI_xx= structure.getRotationalInertia(); + assertEquals(" Booster x-axis MOI is incorrect: ", expMOI_axial, boosterMOI_xx, EPSILON); + + double expMOI_tr = 0.031800928766; + double boosterMOI_tr= structure.getLongitudinalInertia(); + assertEquals(" Booster transverse MOI is incorrect: ", expMOI_tr, boosterMOI_tr, EPSILON); } + + } diff --git a/core/test/net/sf/openrocket/masscalc/MassDataTest.java b/core/test/net/sf/openrocket/masscalc/RigidBodyTest.java similarity index 87% rename from core/test/net/sf/openrocket/masscalc/MassDataTest.java rename to core/test/net/sf/openrocket/masscalc/RigidBodyTest.java index b106d309a..343a8200c 100644 --- a/core/test/net/sf/openrocket/masscalc/MassDataTest.java +++ b/core/test/net/sf/openrocket/masscalc/RigidBodyTest.java @@ -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); } - } diff --git a/core/test/net/sf/openrocket/rocketcomponent/RocketTest.java b/core/test/net/sf/openrocket/rocketcomponent/RocketTest.java index e49fa5637..745ea3a90 100644 --- a/core/test/net/sf/openrocket/rocketcomponent/RocketTest.java +++ b/core/test/net/sf/openrocket/rocketcomponent/RocketTest.java @@ -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); + } + } + } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java index 0a25e0a33..f6ebbe97a 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java @@ -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"); diff --git a/swing/src/net/sf/openrocket/gui/print/PrintableCenteringRing.java b/swing/src/net/sf/openrocket/gui/print/PrintableCenteringRing.java index 4bd361bc1..5f40a8e6d 100644 --- a/swing/src/net/sf/openrocket/gui/print/PrintableCenteringRing.java +++ b/swing/src/net/sf/openrocket/gui/print/PrintableCenteringRing.java @@ -74,7 +74,7 @@ public class PrintableCenteringRing extends AbstractPrintable { List points = new ArrayList(); //Transform the radial positions of the tubes. for (InnerTube it : theMotorMounts) { - if (it.getClusterCount() > 1) { + if (it.getInstanceCount() > 1) { List c = it.getClusterPoints(); for (Coordinate coordinate : c) { points.add(coordinate.setX(it.getOuterRadius())); diff --git a/swing/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java b/swing/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java index 96b5f7497..faf1a2752 100644 --- a/swing/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java +++ b/swing/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java @@ -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); diff --git a/swing/test/net/sf/openrocket/IntegrationTest.java b/swing/test/net/sf/openrocket/IntegrationTest.java index 7700e2245..570d545a6 100644 --- a/swing/test/net/sf/openrocket/IntegrationTest.java +++ b/swing/test/net/sf/openrocket/IntegrationTest.java @@ -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); }