From b30271821131f0411d37c14130545295732b1f4b Mon Sep 17 00:00:00 2001 From: SiboVG Date: Wed, 29 Mar 2023 16:00:45 +0200 Subject: [PATCH] [#2137] Import mass and CG from RASAero sims --- .../importt/RASAeroCommonConstants.java | 20 +- .../rasaero/importt/SimulationHandler.java | 316 +++++++++++++++++- 2 files changed, 321 insertions(+), 15 deletions(-) diff --git a/core/src/net/sf/openrocket/file/rasaero/importt/RASAeroCommonConstants.java b/core/src/net/sf/openrocket/file/rasaero/importt/RASAeroCommonConstants.java index f5e487a70..6a05276e4 100644 --- a/core/src/net/sf/openrocket/file/rasaero/importt/RASAeroCommonConstants.java +++ b/core/src/net/sf/openrocket/file/rasaero/importt/RASAeroCommonConstants.java @@ -112,15 +112,29 @@ public class RASAeroCommonConstants { public static final String SIMULATION_LIST = "SimulationList"; public static final String SIMULATION = "Simulation"; public static final String SUSTAINER_ENGINE = "SustainerEngine"; - // TODO: SustainerLaunchWt, SustainerCG? + public static final String SUSTAINER_LAUNCH_WT = "SustainerLaunchWt"; + public static final String SUSTAINER_NOZZLE_DIAMETER = "SustainerNozzleDiameter"; + public static final String SUSTAINER_CG = "SustainerCG"; public static final String SUSTAINER_IGNITION_DELAY = "SustainerIgnitionDelay"; public static final String BOOSTER1_ENGINE = "Booster1Engine"; public static final String BOOSTER1_SEPARATION_DELAY = "Booster1SeparationDelay"; // Delay after booster burnout to separate public static final String BOOSTER1_IGNITION_DELAY = "Booster1IgnitionDelay"; + public static final String BOOSTER1_LAUNCH_WT = "Booster1LaunchWt"; + public static final String BOOSTER1_NOZZLE_DIAMETER = "Booster1NozzleDiameter"; + public static final String BOOSTER1_CG = "Booster1CG"; public static final String INCLUDE_BOOSTER1 = "IncludeBooster1"; public static final String BOOSTER2_ENGINE = "Booster2Engine"; public static final String BOOSTER2_SEPARATION_DELAY = "Booster2Delay"; // Delay after booster burnout to separate + public static final String BOOSTER2_LAUNCH_WT = "Booster2LaunchWt"; + public static final String BOOSTER2_NOZZLE_DIAMETER = "Booster2NozzleDiameter"; + public static final String BOOSTER2_CG = "Booster2CG"; public static final String INCLUDE_BOOSTER2 = "IncludeBooster2"; + public static final String FLIGHT_TIME = "FlightTime"; + public static final String TIME_TO_APOGEE = "TimetoApogee"; + public static final String MAX_ALTITUDE = "MaxAltitude"; + public static final String MAX_VELOCITY = "MaxVelocity"; + public static final String OPTIMUM_WT = "OptimumWt"; + public static final String OPTIMUM_MAX_ALT = "OptimumMaxAlt"; /** @@ -143,6 +157,10 @@ public class RASAeroCommonConstants { * Angle conversion. RASAero is in degrees, OpenRocket in rad. */ public static final double RASAERO_TO_OPENROCKET_ANGLE = 180 / Math.PI; + /** + * Weight conversion from OpenRocket units to RASAero units. RASAero is in pounds (lb), OpenRocket in kilograms (kg). + */ + public static final double OPENROCKET_TO_RASAERO_WEIGHT = 2.20462262; /** * Temperature conversion. RASAero is in Fahrenheit, OpenRocket in Kelvin. */ diff --git a/core/src/net/sf/openrocket/file/rasaero/importt/SimulationHandler.java b/core/src/net/sf/openrocket/file/rasaero/importt/SimulationHandler.java index b9e6838aa..e803fe6cc 100644 --- a/core/src/net/sf/openrocket/file/rasaero/importt/SimulationHandler.java +++ b/core/src/net/sf/openrocket/file/rasaero/importt/SimulationHandler.java @@ -10,12 +10,14 @@ import net.sf.openrocket.motor.IgnitionEvent; import net.sf.openrocket.motor.Motor; import net.sf.openrocket.motor.MotorConfiguration; import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.rocketcomponent.AxialStage; import net.sf.openrocket.rocketcomponent.FlightConfigurationId; import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.StageSeparationConfiguration; import net.sf.openrocket.simulation.SimulationOptions; +import net.sf.openrocket.util.Coordinate; import org.xml.sax.SAXException; import java.util.HashMap; @@ -33,12 +35,18 @@ public class SimulationHandler extends AbstractElementHandler { // Motor information private ThrustCurveMotor sustainerEngine; private Double sustainerIgnitionDelay; + private Double sustainerLaunchWt; + private Double sustainerCG; private ThrustCurveMotor booster1Engine; private Double booster1IgnitionDelay; private Double booster1SeparationDelay; + private Double booster1LaunchWt; + private Double booster1CG; private Boolean includeBooster1; private ThrustCurveMotor booster2Engine; private Double booster2SeparationDelay; + private Double booster2LaunchWt; + private Double booster2CG; private Boolean includeBooster2; public SimulationHandler(DocumentLoadingContext context, Rocket rocket, SimulationOptions launchSiteSettings, int simulationNr) { @@ -51,9 +59,12 @@ public class SimulationHandler extends AbstractElementHandler { @Override public ElementHandler openElement(String element, HashMap attributes, WarningSet warnings) throws SAXException { if (RASAeroCommonConstants.SUSTAINER_ENGINE.equals(element) || RASAeroCommonConstants.SUSTAINER_IGNITION_DELAY.equals(element) + || RASAeroCommonConstants.SUSTAINER_LAUNCH_WT.equals(element) || RASAeroCommonConstants.SUSTAINER_CG.equals(element) || RASAeroCommonConstants.BOOSTER1_ENGINE.equals(element) || RASAeroCommonConstants.BOOSTER1_IGNITION_DELAY.equals(element) - || RASAeroCommonConstants.BOOSTER1_SEPARATION_DELAY.equals(element) || RASAeroCommonConstants.INCLUDE_BOOSTER1.equals(element) + || RASAeroCommonConstants.BOOSTER1_SEPARATION_DELAY.equals(element) || RASAeroCommonConstants.BOOSTER1_LAUNCH_WT.equals(element) + || RASAeroCommonConstants.BOOSTER1_CG.equals(element) ||RASAeroCommonConstants.INCLUDE_BOOSTER1.equals(element) || RASAeroCommonConstants.BOOSTER2_ENGINE.equals(element) || RASAeroCommonConstants.BOOSTER2_SEPARATION_DELAY.equals(element) + || RASAeroCommonConstants.BOOSTER2_LAUNCH_WT.equals(element) || RASAeroCommonConstants.BOOSTER2_CG.equals(element) || RASAeroCommonConstants.INCLUDE_BOOSTER2.equals(element)) { return PlainTextHandler.INSTANCE; } @@ -66,18 +77,30 @@ public class SimulationHandler extends AbstractElementHandler { sustainerEngine = RASAeroMotorsLoader.getMotorFromRASAero(content, warnings); } else if (RASAeroCommonConstants.SUSTAINER_IGNITION_DELAY.equals(element)) { sustainerIgnitionDelay = Double.parseDouble(content); + } else if (RASAeroCommonConstants.SUSTAINER_LAUNCH_WT.equals(element)) { + sustainerLaunchWt = Double.parseDouble(content) / RASAeroCommonConstants.OPENROCKET_TO_RASAERO_WEIGHT; + } else if (RASAeroCommonConstants.SUSTAINER_CG.equals(element)) { + sustainerCG = Double.parseDouble(content) / RASAeroCommonConstants.RASAERO_TO_OPENROCKET_LENGTH; } else if (RASAeroCommonConstants.BOOSTER1_ENGINE.equals(element)) { booster1Engine = RASAeroMotorsLoader.getMotorFromRASAero(content, warnings); } else if (RASAeroCommonConstants.BOOSTER1_IGNITION_DELAY.equals(element)) { booster1IgnitionDelay = Double.parseDouble(content); } else if (RASAeroCommonConstants.BOOSTER1_SEPARATION_DELAY.equals(element)) { booster1SeparationDelay = Double.parseDouble(content); + } else if (RASAeroCommonConstants.BOOSTER1_LAUNCH_WT.equals(element)) { + booster1LaunchWt = Double.parseDouble(content) / RASAeroCommonConstants.OPENROCKET_TO_RASAERO_WEIGHT; + } else if (RASAeroCommonConstants.BOOSTER1_CG.equals(element)) { + booster1CG = Double.parseDouble(content) / RASAeroCommonConstants.RASAERO_TO_OPENROCKET_LENGTH; } else if (RASAeroCommonConstants.INCLUDE_BOOSTER1.equals(element)) { includeBooster1 = Boolean.parseBoolean(content); } else if (RASAeroCommonConstants.BOOSTER2_ENGINE.equals(element)) { booster2Engine = RASAeroMotorsLoader.getMotorFromRASAero(content, warnings); } else if (RASAeroCommonConstants.BOOSTER2_SEPARATION_DELAY.equals(element)) { booster2SeparationDelay = Double.parseDouble(content); + } else if (RASAeroCommonConstants.BOOSTER2_LAUNCH_WT.equals(element)) { + booster2LaunchWt = Double.parseDouble(content) / RASAeroCommonConstants.OPENROCKET_TO_RASAERO_WEIGHT; + } else if (RASAeroCommonConstants.BOOSTER2_CG.equals(element)) { + booster2CG = Double.parseDouble(content) / RASAeroCommonConstants.RASAERO_TO_OPENROCKET_LENGTH; } else if (RASAeroCommonConstants.INCLUDE_BOOSTER2.equals(element)) { includeBooster2 = Boolean.parseBoolean(content); } @@ -85,48 +108,63 @@ public class SimulationHandler extends AbstractElementHandler { @Override public void endHandler(String element, HashMap attributes, String content, WarningSet warnings) throws SAXException { - FlightConfigurationId id = new FlightConfigurationId(); - rocket.createFlightConfiguration(id); + FlightConfigurationId fcid = new FlightConfigurationId(); + rocket.createFlightConfiguration(fcid); // Select if this is the first config if (rocket.getSelectedConfiguration().getId().equals(FlightConfigurationId.DEFAULT_VALUE_FCID)) { - rocket.setSelectedConfiguration(id); + rocket.setSelectedConfiguration(fcid); } // Add motors to the rocket - addMotorToStage(0, sustainerEngine, sustainerIgnitionDelay, id, warnings); + MotorMount sustainerMount = addMotorToStage(0, sustainerEngine, sustainerIgnitionDelay, fcid, warnings); + MotorMount booster1Mount = null; if (includeBooster1) { - addMotorToStage(1, booster1Engine, booster1IgnitionDelay, id, warnings); + booster1Mount = addMotorToStage(1, booster1Engine, booster1IgnitionDelay, fcid, warnings); } + MotorMount booster2Mount = null; if (includeBooster2) { - addMotorToStage(2, booster2Engine, 0.0, id, warnings); + booster2Mount = addMotorToStage(2, booster2Engine, 0.0, fcid, warnings); } // Set separation settings - setSeparationDelay(0, 0.0, id); + setSeparationDelay(0, 0.0, fcid); if (includeBooster1) { - setSeparationDelay(1, booster1SeparationDelay, id); + setSeparationDelay(1, booster1SeparationDelay, fcid); } if (includeBooster2) { - setSeparationDelay(2, booster2SeparationDelay, id); + setSeparationDelay(2, booster2SeparationDelay, fcid); } // Add a new simulation Simulation sim = new Simulation(rocket); - sim.setFlightConfigurationId(id); + sim.setFlightConfigurationId(fcid); sim.setName("Simulation " + simulationNr); sim.copySimulationOptionsFrom(launchSiteSettings); context.getOpenRocketDocument().addSimulation(sim); + + // Set the weight and CG overrides + applyMassOverrides(); + applyCGOverrides(sustainerMount, booster1Mount, booster2Mount, fcid); } - private void addMotorToStage(final int stageNr, final Motor motor, final Double ignitionDelay, + /** + * Adds a motor to the specified stage. + * @param stageNr The stage number + * @param motor The motor to add + * @param ignitionDelay The ignition delay + * @param id The flight configuration id + * @param warnings The warning set + * @return The motor mount in which the motor is added, or null if no motor was added + */ + private MotorMount addMotorToStage(final int stageNr, final Motor motor, final Double ignitionDelay, final FlightConfigurationId id, final WarningSet warnings) { if (motor == null || rocket.getStage(stageNr) == null) { - return; + return null; } MotorMount mount = getMotorMountForStage(stageNr); if (mount == null) { warnings.add("No motor mount found for stage " + stageNr + ". Ignoring motor."); - return; + return null; } MotorConfiguration motorConfig = new MotorConfiguration(mount, id); motorConfig.setMotor(motor); @@ -145,6 +183,8 @@ public class SimulationHandler extends AbstractElementHandler { motorConfig.setIgnitionEvent(IgnitionEvent.AUTOMATIC); } mount.setMotorConfig(motorConfig, id); + + return mount; } private void setSeparationDelay(final int stageNr, Double separationDelay, @@ -176,4 +216,252 @@ public class SimulationHandler extends AbstractElementHandler { } return mount; } + + private void applyMassOverrides() { + // Don't do anything if the mass has already been overridden by a previous simulation + if (rocket.getStage(0).isMassOverridden()) { + return; + } + + double sustainerMass = applySustainerMassOverride(); + double booster1Mass = applyBooster1MassOverride(sustainerMass); + applyBooster2MassOverride(sustainerMass, booster1Mass); + } + + /** + * Applies the mass from the RASAero simulation to the sustainer as an override, and returns the final sustainer mass. + * Note: the sustainer motor mass is subtracted from the RASAero mass to get the final mass. + * @return the final sustainer mass + */ + private double applySustainerMassOverride() { + if (sustainerLaunchWt == null || sustainerLaunchWt == 0) { + return 0; + } + + // Get the sustainer motor weight + double sustainerMotorWt = 0; + if (sustainerEngine != null) { + sustainerMotorWt = sustainerEngine.getLaunchMass(); + } + + double sustainerWt = sustainerLaunchWt - sustainerMotorWt; + AxialStage sustainer = rocket.getStage(0); + sustainer.setMassOverridden(true); + sustainer.setSubcomponentsOverriddenMass(true); + sustainer.setOverrideMass(sustainerWt); + + return sustainerWt; + } + + /** + * Applies the mass from the RASAero simulation to booster1 as an override, and returns the final booster1 mass. + * Note: the sustainer mass and booster1 motor mass is subtracted from the RASAero mass to get the final mass. + * @param sustainerMass the sustainer mass + * @return the final booster1 mass + */ + private double applyBooster1MassOverride(double sustainerMass) { + if (!includeBooster1 || booster1LaunchWt == null || booster1LaunchWt == 0) { + return 0; + } + + // Get the booster motor weight + double boosterMotorWt = 0; + if (booster1Engine != null) { + boosterMotorWt = booster1Engine.getLaunchMass(); + } + + double boosterWt = booster1LaunchWt - boosterMotorWt - sustainerMass; + AxialStage booster = rocket.getStage(1); + booster.setMassOverridden(true); + booster.setSubcomponentsOverriddenMass(true); + booster.setOverrideMass(boosterWt); + + return boosterWt; + } + + /** + * Applies the mass from the RASAero simulation to booster2 as an override, and returns the final booster2 mass. + * Note: the sustainer mass, booster1 mass, and booster2 motor mass is subtracted from the RASAero mass to get the final mass. + * @param sustainerMass the sustainer mass + * @param booster1Mass the booster1 mass + * @return the final booster2 mass + */ + private double applyBooster2MassOverride(double sustainerMass, double booster1Mass) { + if (!includeBooster2 || booster2LaunchWt == null || booster2LaunchWt == 0) { + return 0; + } + + // Get the booster motor weight + double boosterMotorWt = 0; + if (booster2Engine != null) { + boosterMotorWt = booster2Engine.getLaunchMass(); + } + + double boosterWt = booster2LaunchWt - boosterMotorWt - sustainerMass - booster1Mass; + AxialStage booster = rocket.getStage(2); + booster.setMassOverridden(true); + booster.setSubcomponentsOverriddenMass(true); + booster.setOverrideMass(boosterWt); + + return boosterWt; + } + + /** + * Applies the CG extracted from the simulation to the sustainer, booster1, and booster2. + * @param sustainerMount the sustainer motor mount + * @param booster1Mount the booster1 motor mount + * @param booster2Mount the booster2 motor mount + * @param fcid the flight configuration ID of this simulation + */ + private void applyCGOverrides(MotorMount sustainerMount, MotorMount booster1Mount, MotorMount booster2Mount, + FlightConfigurationId fcid) { + // Don't do anything if the CG has already been overridden by a previous simulation + if (rocket.getStage(0).isCGOverridden()) { + return; + } + + applySustainerCGOverride(sustainerMount, fcid); + applyBooster1CGOverride(booster1Mount, fcid); + applyBooster2CGOverride(booster2Mount, fcid); + } + + /** + * Applies the CG from the RASAero simulation to the sustainer as an override. + * This CG value can be applied directly. + * @param sustainerMount the sustainer motor mount + * @param fcid the flight configuration ID of this simulation + * @return the CG of the sustainer + */ + private Double applySustainerCGOverride(MotorMount sustainerMount, FlightConfigurationId fcid) { + if (sustainerCG == null) { + return null; + } + + AxialStage sustainer = rocket.getStage(0); + + /* + sustainerCG is the combined CG of the sustainer and its motor (if present), + so we need to calculate the CG of the sustainer without the motor CG + */ + Double CG = getStageCGWithoutMotorCG(sustainer, sustainerCG, sustainerMount, sustainerEngine, fcid); + + sustainer.setCGOverridden(true); + sustainer.setSubcomponentsOverriddenCG(true); + sustainer.setOverrideCGX(CG); + + return CG; + } + + /** + * Applies the CG from the RASAero simulation to booster1 as an override. + * The CG value returned by RASAero is the aggregate CG of the sustainer and booster1. + * To calculate the CG of booster1, we use the following formula: + * booster1 CG = [booster1 mass x (distance between CG of sustainer and combined CG)] / (sustainer mass) + * @param booster1Mount the booster1 motor mount + * @param fcid the flight configuration ID of this simulation + * @return the CG of booster1 + */ + private Double applyBooster1CGOverride(MotorMount booster1Mount, FlightConfigurationId fcid) { + if (!includeBooster1 || booster1CG == null || sustainerCG == null || booster1LaunchWt == null || sustainerLaunchWt == null) { + return null; + } + + AxialStage booster = rocket.getStage(1); + + // Do a back-transform of the combined CG of the sustainer and booster1 to get the CG of booster1 + Double CG = getCGFromCombinedCG(booster1CG, sustainerCG, sustainerLaunchWt, booster1LaunchWt - sustainerLaunchWt); + // ! this CG is relative to the front of the sustainer, we need it referenced to the front of the booster + CG = CG - booster.getPosition().x; + + /* + the above CG is the combined CG of booster1 and its motor (if present), + so we need to calculate the CG of booster1 without the motor CG + */ + CG = getStageCGWithoutMotorCG(booster, CG, booster1Mount, booster1Engine, fcid); + + booster.setCGOverridden(true); + booster.setSubcomponentsOverriddenCG(true); + booster.setOverrideCGX(CG); + + return CG; + } + + /** + * Applies the CG from the RASAero simulation to booster2 as an override. + * The CG value returned by RASAero is the aggregate CG of the sustainer, booster1, and booster2. + * To calculate the CG of booster2, we use the following formula: + * booster2 CG = [booster2 mass x (distance between CG of sustainer and combined CG)] / (sustainer mass + booster1 mass) + * @param booster2Mount the booster1 motor mount + * @param fcid the flight configuration ID of this simulation + * @return the CG of booster2 + */ + private Double applyBooster2CGOverride(MotorMount booster2Mount, FlightConfigurationId fcid) { + if (!includeBooster2 || booster2CG == null || booster1CG == null || sustainerCG == null || + booster2LaunchWt == null || booster1LaunchWt == null || sustainerLaunchWt == null) { + return null; + } + + AxialStage booster = rocket.getStage(2); + + // Do a back-transform of the combined CG of the sustainer, booster1, and booster2 to get the CG of booster2 + Double CG = getCGFromCombinedCG(booster2CG, booster1CG, booster1LaunchWt, booster2LaunchWt - booster1LaunchWt); + // ! this CG is relative to the front of the sustainer, we need it referenced to the front of the booster + CG = CG - booster.getPosition().x; + + /* + the above CG is the combined CG of booster2 and its motor (if present), + so we need to calculate the CG of booster2 without the motor CG + */ + CG = getStageCGWithoutMotorCG(booster, CG, booster2Mount, booster2Engine, fcid); + + booster.setCGOverridden(true); + booster.setSubcomponentsOverriddenCG(true); + booster.setOverrideCGX(CG); + + return CG; + } + + /** + * Return the CG of object B from the CG of object A, the combined CG of A and B, and the masses of A and B. + * @param combinedCG the combined CG of A and B + * @param objACG the CG of object A + * @param objAMass the mass of object A + * @param objBMass the mass of object B + * @return the CG of object B + */ + private Double getCGFromCombinedCG(Double combinedCG, Double objACG, Double objAMass, Double objBMass) { + if (combinedCG == null || objACG == null || objAMass == null || objBMass == null) { + return combinedCG; + } + return combinedCG * (1 + objAMass / objBMass) - objACG * (objAMass / objBMass); + } + + /** + * Extracts the CG of a stage from a combined CG of the stage and stage motor. + * @param stage the stage + * @param combinedCG the combiend CG of the stage and its motor + * @param mount the motor mount of the stage that holds the motor + * @param motor the motor + * @param fcid the flight configuration ID of this simulation + * @return the CG of the stage without the motor CG + */ + private Double getStageCGWithoutMotorCG(AxialStage stage, Double combinedCG, MotorMount mount, ThrustCurveMotor motor, + FlightConfigurationId fcid) { + if (motor == null || mount == null || stage == null || combinedCG == null) { + return combinedCG; + } + double stageMass = stage.getMass(); // Should be overridden by now, so don't use getSectionMass() + double motorMass = motor.getLaunchMass(); + + Coordinate[] CGPoints = motor.getCGPoints(); + if (CGPoints != null && CGPoints.length > 1) { + double motorPositionXRel = mount.getMotorPosition(fcid).x; // Motor position relative to the mount + double mountLocationX = mount.getLocations()[0].x; + double motorLocationX = mountLocationX + motorPositionXRel; + double motorCG = motor.getCGPoints()[0].x + motorLocationX; + return combinedCG * (1 + motorMass / stageMass) + - motorCG * (motorMass / stageMass); + } + return null; + } }