--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>\r
+<classpath>\r
+ <classpathentry kind="src" path="src"/>\r
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>\r
+ <classpathentry kind="lib" path="jscience.jar"/>\r
+ <classpathentry kind="lib" path="jfreechart-1.0.12.jar"/>\r
+ <classpathentry kind="lib" path="jcommon-1.0.15.jar"/>\r
+ <classpathentry kind="lib" path="log4j-1.2.15.jar"/>\r
+ <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>\r
+ <classpathentry kind="output" path="bin"/>\r
+</classpath>\r
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>\r
+<projectDescription>\r
+ <name>MotorSim</name>\r
+ <comment></comment>\r
+ <projects>\r
+ </projects>\r
+ <buildSpec>\r
+ <buildCommand>\r
+ <name>org.eclipse.jdt.core.javabuilder</name>\r
+ <arguments>\r
+ </arguments>\r
+ </buildCommand>\r
+ </buildSpec>\r
+ <natures>\r
+ <nature>org.eclipse.jdt.core.javanature</nature>\r
+ </natures>\r
+</projectDescription>\r
--- /dev/null
+log4j.rootCategory=INFO, O\r
+\r
+# Stdout\r
+log4j.appender.O=org.apache.log4j.ConsoleAppender\r
+log4j.appender.O.layout=org.apache.log4j.PatternLayout
\ No newline at end of file
--- /dev/null
+import javax.measure.quantity.Dimensionless;\r
+import javax.measure.quantity.Length;\r
+import javax.measure.unit.SI;\r
+\r
+import org.jscience.physics.amount.Amount;\r
+\r
+\r
+public class Test {\r
+ public static void main(String args[]){\r
+ Amount<Length> l = (Amount<Length>)Amount.valueOf(1, SI.METER);\r
+ System.out.println(l.divide(l).to(Dimensionless.UNIT));\r
+ Amount<Length> m = (Amount<Length>)Amount.valueOf(1, 10000000000000000.0, SI.METER);\r
+ System.out.println(m);\r
+ \r
+ }\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim;\r
+\r
+\r
+\r
+import java.util.SortedMap;\r
+import java.util.TreeMap;\r
+\r
+import javax.measure.quantity.Area;\r
+import javax.measure.quantity.Dimensionless;\r
+import javax.measure.quantity.Duration;\r
+import javax.measure.quantity.Force;\r
+import javax.measure.quantity.Length;\r
+import javax.measure.quantity.Mass;\r
+import javax.measure.quantity.MassFlowRate;\r
+import javax.measure.quantity.Pressure;\r
+import javax.measure.quantity.Temperature;\r
+import javax.measure.quantity.Velocity;\r
+import javax.measure.quantity.Volume;\r
+import javax.measure.quantity.VolumetricDensity;\r
+import javax.measure.unit.SI;\r
+\r
+import org.apache.log4j.Logger;\r
+import org.jscience.physics.amount.Amount;\r
+import org.jscience.physics.amount.Constants;\r
+\r
+import com.billkuker.rocketry.motorsim.fuel.KNSU;\r
+import com.billkuker.rocketry.motorsim.grain.CoredCylindricalGrain;\r
+import com.billkuker.rocketry.motorsim.grain.ExtrudedGrain;\r
+import com.billkuker.rocketry.motorsim.visual.Chart;\r
+\r
+public class Burn {\r
+ \r
+ private static Logger log = Logger.getLogger(Burn.class);\r
+ protected final Motor motor;\r
+ \r
+ private static final Amount<Pressure> atmosphereicPressure = Amount.valueOf(101000, SI.PASCAL);\r
+ \r
+ \r
+ private static double combustionEfficency = 0.97;\r
+ \r
+ private static double densityRatio = 0.96;\r
+ \r
+ protected class Interval{\r
+ Amount<Duration> time;\r
+ Amount<Length> regression;\r
+ Amount<Pressure> chamberPressure;\r
+ Amount<Mass> chamberProduct;\r
+ Amount<Force> thrust;\r
+\r
+ public String toString(){\r
+ return time + " " + regression + " " + chamberPressure + " " + chamberProduct;\r
+ }\r
+ }\r
+ \r
+ protected SortedMap<Amount<Duration>,Interval> data = new TreeMap<Amount<Duration>, Interval>();\r
+ \r
+ public Burn(Motor m){\r
+ motor = m;\r
+ }\r
+ \r
+ private void burn(){\r
+ log.info("Starting burn...");\r
+ \r
+ Amount<Length> regStep = Amount.valueOf(0.0119904077, SI.MILLIMETER);\r
+\r
+ //if ( motor.getGrain() instanceof Grain.DiscreteRegression )\r
+ //regStep = ((Grain.DiscreteRegression)motor.getGrain()).optimalRegressionStep();\r
+ \r
+ Interval initial = new Interval();\r
+ initial.time = Amount.valueOf(0, SI.SECOND);\r
+ initial.regression = Amount.valueOf(0, SI.MILLIMETER);\r
+ initial.chamberPressure = atmosphereicPressure;\r
+ initial.chamberProduct = Amount.valueOf(0, SI.KILOGRAM);\r
+ initial.thrust = Amount.valueOf(0, SI.NEWTON);\r
+ \r
+ data.put(Amount.valueOf(0, SI.SECOND), initial);\r
+ \r
+ for ( int i = 0; i < 5000; i++ ){\r
+\r
+ Interval prev = data.get(data.lastKey());\r
+ log.debug(prev);\r
+ log.debug("Step " + i + " ==============================");\r
+ Interval next = new Interval();\r
+ \r
+ Amount<Velocity> burnRate = motor.getFuel().burnRate(prev.chamberPressure);\r
+ \r
+ log.debug("Burn Rate: " + burnRate);\r
+ \r
+ Amount<Duration> dt = regStep.divide(burnRate).to(Duration.UNIT);\r
+ \r
+ data.put(data.lastKey().plus(dt), next);\r
+ \r
+ log.debug("Dt: " + dt);\r
+ \r
+ next.regression = prev.regression.plus(regStep);\r
+ \r
+ log.info("Regression: " + next.regression);\r
+ \r
+ next.time = prev.time.plus(dt);\r
+ \r
+ log.debug("Vold: " + motor.getGrain().volume(prev.regression).to(SI.MILLIMETER.pow(3)));\r
+ \r
+ log.debug("Vnew: " + motor.getGrain().volume(next.regression).to(SI.MILLIMETER.pow(3)));\r
+ \r
+ //TODO Amount<Volume> volumeBurnt = motor.getGrain().volume(prev.regression).minus(motor.getGrain().volume(next.regression));\r
+ Amount<Volume> volumeBurnt = motor.getGrain().surfaceArea(prev.regression).times(regStep).to(Volume.UNIT);\r
+ \r
+ log.info("Volume Burnt: " + volumeBurnt.to(SI.MILLIMETER.pow(3)));\r
+ \r
+ Amount<MassFlowRate> mGenRate = volumeBurnt.times(motor.getFuel().idealDensity().times(densityRatio)).divide(dt).to(MassFlowRate.UNIT);\r
+ \r
+ log.debug("Mass Gen Rate: " + mGenRate);\r
+ \r
+ Amount specificGasConstant = Constants.R.divide(motor.getFuel().getCombustionProduct().effectiveMolarWeight());\r
+ Amount<Temperature> chamberTemp = motor.getFuel().getCombustionProduct().idealCombustionTemperature().times(combustionEfficency);\r
+ \r
+ Amount<MassFlowRate> mNozzle;\r
+ {\r
+ Amount<Pressure> pDiff = prev.chamberPressure.minus(atmosphereicPressure);\r
+ \r
+ //pDiff = Amount.valueOf(.7342, MPA).minus(atmosphereicPressure);\r
+ \r
+ log.debug("Pdiff: " + pDiff);\r
+ \r
+ Amount<Area> aStar = motor.getNozzle().throatArea();\r
+ \r
+ double k = motor.getFuel().getCombustionProduct().ratioOfSpecificHeats();\r
+ \r
+ log.debug("K: " + k);\r
+ \r
+ double kSide = Math.sqrt(k) * Math.pow((2/(k+1)) , (((k+1)/2)/(k-1))); //Math.pow(2/k+1, (k+1)/(2*(k-1)));\r
+ \r
+ log.debug("K-Part: (good)" + kSide);\r
+ \r
+ \r
+ \r
+ //This unit conversion helps JScience to convert nozzle flow rate to\r
+ //kg/s a little later on I verified the conversion by hand and\r
+ //JScience checks it too.\r
+ specificGasConstant = specificGasConstant.to(\r
+ SI.METER.pow(2).divide(SI.SECOND.pow(2).times(SI.KELVIN)));\r
+ \r
+ log.debug("Specific Gas Constant: (good)" + specificGasConstant);\r
+ \r
+ Amount sqrtPart = specificGasConstant.times(chamberTemp).sqrt();\r
+\r
+ //Unit x = SI.JOULE.divide(SI.KILOGRAM).root(2);\r
+ \r
+ //sqrtPart = sqrtPart.times(Amount.valueOf(1, x));\r
+ \r
+ log.debug("Square Root Part: " + sqrtPart);\r
+ \r
+ mNozzle = pDiff.times(aStar).times(kSide).divide(sqrtPart).to(MassFlowRate.UNIT);\r
+ \r
+ log.debug("Nozzle Flow: " + mNozzle);\r
+ \r
+ log.debug("Nozzle Flow: " + mNozzle.to(MassFlowRate.UNIT));\r
+ \r
+ \r
+ \r
+ \r
+ }\r
+ \r
+ Amount<MassFlowRate> massStorageRate = mGenRate.minus(mNozzle);\r
+ \r
+ log.debug("Chamber Product rate: " + massStorageRate);\r
+ \r
+ next.chamberProduct = prev.chamberProduct.plus(massStorageRate.times(dt));\r
+ \r
+ log.debug("Chamber Product: " + next.chamberProduct);\r
+ \r
+ Amount<VolumetricDensity> combustionProductDensity = next.chamberProduct.divide(motor.getChamber().chamberVolume().minus(motor.getGrain().volume(next.regression))).to(VolumetricDensity.UNIT);\r
+ \r
+ log.debug("Product Density: " + combustionProductDensity);\r
+ \r
+ next.chamberPressure = combustionProductDensity.times(specificGasConstant).times(chamberTemp).plus(atmosphereicPressure).to(Pressure.UNIT);\r
+ \r
+ next.chamberPressure = Amount.valueOf(\r
+ next.chamberPressure.doubleValue(SI.PASCAL),\r
+ SI.PASCAL);\r
+ \r
+ next.thrust = motor.getNozzle().thrust(next.chamberPressure, atmosphereicPressure, atmosphereicPressure, motor.getFuel().getCombustionProduct().ratioOfSpecificHeats2Phase());\r
+ \r
+ if ( next.chamberPressure.approximates(atmosphereicPressure)){\r
+ log.info("Pressure at Patm on step " + i);\r
+ break;\r
+ }\r
+ }\r
+ \r
+ }\r
+ \r
+ public Amount<Pressure> pressure(Amount<Duration> time){\r
+ return data.get(time).chamberPressure;\r
+ }\r
+ \r
+ public Amount<Force> thrust(Amount<Duration> time){\r
+ return data.get(time).thrust;\r
+ }\r
+ \r
+ public Amount<Dimensionless> kn(Amount<Length> regression){\r
+ return motor.getGrain().surfaceArea(regression).divide(motor.getNozzle().throatArea()).to(Dimensionless.UNIT);\r
+ }\r
+ \r
+ public static void main( String args[]) throws Exception{\r
+ Motor m = new Motor();\r
+ m.setFuel(new KNSU());\r
+ \r
+ CylindricalChamber c = new CylindricalChamber();\r
+ c.setLength(Amount.valueOf(100, SI.MILLIMETER));\r
+ c.setID(Amount.valueOf(30, SI.MILLIMETER));\r
+ m.setChamber(c);\r
+ \r
+ CoredCylindricalGrain g = new CoredCylindricalGrain();\r
+ g.setLength(Amount.valueOf(70, SI.MILLIMETER));\r
+ g.setOD(Amount.valueOf(30, SI.MILLIMETER));\r
+ g.setID(Amount.valueOf(10, SI.MILLIMETER));\r
+ m.setGrain(g);\r
+ \r
+ m.setGrain(new ExtrudedGrain());\r
+ \r
+ ConvergentDivergentNozzle n = new ConvergentDivergentNozzle();\r
+ n.setThroatDiameter(Amount.valueOf(6.600, SI.MILLIMETER));\r
+ n.setExitDiameter(Amount.valueOf(20.87, SI.MILLIMETER));\r
+ n.setEfficiency(.87);\r
+ m.setNozzle(n);\r
+ \r
+ Burn b = new Burn(m);\r
+ \r
+ b.burn();\r
+ \r
+ Chart<Duration, Pressure> r = new Chart<Duration, Pressure>(\r
+ SI.SECOND,\r
+ SI.MEGA(SI.PASCAL),\r
+ b,\r
+ "pressure");\r
+ r.setDomain(b.data.keySet());\r
+ r.show();\r
+ \r
+ Chart<Duration, Force> t = new Chart<Duration, Force>(\r
+ SI.SECOND,\r
+ SI.NEWTON,\r
+ b,\r
+ "thrust");\r
+ t.setDomain(b.data.keySet());\r
+ t.show();\r
+ \r
+ /*\r
+ Chart<Length, Area> s = new Chart<Length, Area>(SI.MILLIMETER,\r
+ SI.MILLIMETER.pow(2).asType(Area.class), g, "surfaceArea");\r
+ s.setDomain(s.new IntervalDomain(Amount.valueOf(0, SI.CENTIMETER), g\r
+ .webThickness()));\r
+ s.show();\r
+ */\r
+ \r
+ Chart<Length, Dimensionless> kn = new Chart<Length, Dimensionless>(SI.MILLIMETER,\r
+ Dimensionless.UNIT, b, "kn");\r
+ kn.setDomain(kn.new IntervalDomain(Amount.valueOf(0, SI.CENTIMETER), m.getGrain()\r
+ .webThickness()));\r
+ kn.show();\r
+ \r
+ }\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim;\r
+\r
+import javax.measure.quantity.Pressure;\r
+import javax.measure.quantity.Volume;\r
+\r
+import org.jscience.physics.amount.Amount;\r
+\r
+public interface Chamber {\r
+ public Amount<Volume> chamberVolume();\r
+ \r
+ public Amount<Pressure> burstPressure();\r
+ \r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim;\r
+\r
+import javax.measure.quantity.Area;\r
+import javax.measure.quantity.Dimensionless;\r
+import javax.measure.quantity.Force;\r
+import javax.measure.quantity.Length;\r
+import javax.measure.quantity.Pressure;\r
+\r
+import org.jscience.physics.amount.Amount;\r
+\r
+public class ConvergentDivergentNozzle implements Nozzle {\r
+\r
+ private Amount<Length> throatDiameter;\r
+ \r
+ private Amount<Length> exitDiameter;\r
+ \r
+ private double efficiency = 1.0;\r
+ \r
+ \r
+ @Override\r
+ public Amount<Area> throatArea() {\r
+ return throatDiameter.divide(2).pow(2).times(Math.PI).to(Area.UNIT);\r
+ }\r
+ \r
+ public Amount<Area> exitArea() {\r
+ return exitDiameter.divide(2).pow(2).times(Math.PI).to(Area.UNIT);\r
+ }\r
+\r
+\r
+ public Amount<Length> getThroatDiameter() {\r
+ return throatDiameter;\r
+ }\r
+\r
+\r
+ public void setThroatDiameter(Amount<Length> throatDiameter) {\r
+ this.throatDiameter = throatDiameter;\r
+ }\r
+ \r
+\r
+ public Amount<Length> getExitDiameter() {\r
+ return exitDiameter;\r
+ }\r
+\r
+\r
+ public void setExitDiameter(Amount<Length> exitDiameter) {\r
+ this.exitDiameter = exitDiameter;\r
+ }\r
+ \r
+ @Override\r
+ public Amount<Force> thrust(Amount<Pressure> Po, Amount<Pressure> Pe, Amount<Pressure> Patm, final double k ){\r
+ double cF = thrustCoefficient(Po, Pe, Patm, k);\r
+ return Po.times(throatArea()).times(cF).to(Force.UNIT);\r
+ }\r
+\r
+ public double thrustCoefficient(Amount<Pressure> Po, Amount<Pressure> Pe, Amount<Pressure> Patm, final double k ){\r
+ double pRatio = Pe.divide(Po).to(Dimensionless.UNIT).doubleValue(Dimensionless.UNIT);\r
+\r
+ double a = (2*k*k)/(k-1);\r
+ \r
+ double b = Math.pow(2/(k+1), (k+1)/(k-1) );\r
+ \r
+ double c = 1.0 - Math.pow(pRatio, (k-1)/k);\r
+ \r
+ Amount<Pressure> pDiff = Pe.minus(Patm);\r
+ \r
+ double d = pDiff.times(exitArea()).divide(Po.times(throatArea())).to(Dimensionless.UNIT).doubleValue(Dimensionless.UNIT);\r
+ \r
+ double Cf = efficiency * Math.sqrt(a*b*c)+d;\r
+ \r
+ return Cf;\r
+ }\r
+\r
+ public double getEfficiency() {\r
+ return efficiency;\r
+ }\r
+\r
+ public void setEfficiency(double efficiency) {\r
+ this.efficiency = efficiency;\r
+ }\r
+\r
+\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim;\r
+\r
+import javax.measure.quantity.Length;\r
+import javax.measure.quantity.Pressure;\r
+import javax.measure.quantity.Volume;\r
+import javax.measure.unit.SI;\r
+\r
+import org.jscience.physics.amount.Amount;\r
+\r
+public class CylindricalChamber implements Chamber {\r
+ \r
+ private Amount<Length> length;\r
+ \r
+ private Amount<Length> iD;\r
+\r
+ @Override\r
+ public Amount<Pressure> burstPressure() {\r
+ return null;\r
+ }\r
+\r
+ @Override\r
+ public Amount<Volume> chamberVolume() {\r
+ return iD.divide(2).pow(2).times(Math.PI).times(length).to(SI.CUBIC_METRE);\r
+ }\r
+\r
+ public Amount<Length> getLength() {\r
+ return length;\r
+ }\r
+\r
+ public void setLength(Amount<Length> length) {\r
+ this.length = length;\r
+ }\r
+\r
+ public Amount<Length> getID() {\r
+ return iD;\r
+ }\r
+\r
+ public void setID(Amount<Length> id) {\r
+ iD = id;\r
+ }\r
+\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim;\r
+\r
+import javax.measure.quantity.Pressure;\r
+import javax.measure.quantity.Temperature;\r
+import javax.measure.quantity.Velocity;\r
+import javax.measure.quantity.VolumetricDensity;\r
+\r
+import org.jscience.physics.amount.Amount;\r
+\r
+import com.billkuker.rocketry.motorsim.RocketScience.MolarWeight;\r
+\r
+public interface Fuel {\r
+\r
+ public Amount<VolumetricDensity> idealDensity();\r
+\r
+ public Amount<Velocity> burnRate(Amount<Pressure> pressure);\r
+ \r
+ public CombustionProduct getCombustionProduct();\r
+ \r
+ public interface CombustionProduct {\r
+ public Amount<Temperature> idealCombustionTemperature();\r
+\r
+ public Amount<MolarWeight> effectiveMolarWeight();\r
+ \r
+ public double ratioOfSpecificHeats();\r
+ \r
+ public double ratioOfSpecificHeats2Phase();\r
+ }\r
+\r
+ /*\r
+ @Deprecated\r
+ public Amount<Temperature> idealCombustionTemperature();\r
+\r
+ @Deprecated\r
+ public Amount<MolarWeight> effectiveMolarWeight();\r
+ \r
+ @Deprecated\r
+ public double ratioOfSpecificHeats();\r
+ \r
+ @Deprecated\r
+ public double ratioOfSpecificHeats2Phase();*/\r
+\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim;\r
+\r
+import java.awt.Graphics2D;\r
+\r
+import javax.measure.quantity.Area;\r
+import javax.measure.quantity.Length;\r
+import javax.measure.quantity.Volume;\r
+\r
+import org.jscience.physics.amount.Amount;\r
+\r
+public interface Grain {\r
+ \r
+ public interface Graphical{\r
+ public void draw( Graphics2D g, Amount<Length> regression );\r
+ }\r
+ \r
+ public interface DiscreteRegression{\r
+ public Amount<Length> optimalRegressionStep();\r
+ }\r
+\r
+ public Amount<Area> surfaceArea(Amount<Length> regression);\r
+ \r
+ public Amount<Volume> volume(Amount<Length> regression);\r
+ \r
+ public Amount<Length> webThickness();\r
+\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim;\r
+\r
+public class Motor {\r
+ private Chamber chamber;\r
+ private Grain grain;\r
+ private Nozzle nozzle;\r
+ private Fuel fuel;\r
+ \r
+ public Chamber getChamber() {\r
+ return chamber;\r
+ }\r
+ \r
+ public void setChamber(Chamber chamber) {\r
+ this.chamber = chamber;\r
+ }\r
+ \r
+ public Grain getGrain() {\r
+ return grain;\r
+ }\r
+ \r
+ public void setGrain(Grain grain) {\r
+ this.grain = grain;\r
+ }\r
+ \r
+ public Nozzle getNozzle() {\r
+ return nozzle;\r
+ }\r
+ \r
+ public void setNozzle(Nozzle nozzle) {\r
+ this.nozzle = nozzle;\r
+ }\r
+\r
+ public Fuel getFuel() {\r
+ return fuel;\r
+ }\r
+\r
+ public void setFuel(Fuel fuel) {\r
+ this.fuel = fuel;\r
+ }\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim;\r
+\r
+import javax.measure.quantity.Area;\r
+import javax.measure.quantity.Force;\r
+import javax.measure.quantity.Pressure;\r
+\r
+import org.jscience.physics.amount.Amount;\r
+\r
+public interface Nozzle {\r
+ public Amount<Area> throatArea();\r
+ \r
+ public Amount<Force> thrust(Amount<Pressure> Po, Amount<Pressure> Pe, Amount<Pressure> Patm, final double k );\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim;\r
+\r
+import javax.measure.quantity.Quantity;\r
+import javax.measure.unit.ProductUnit;\r
+import javax.measure.unit.SI;\r
+import javax.measure.unit.Unit;\r
+\r
+public class RocketScience {\r
+\r
+ public interface MolarWeight extends Quantity {\r
+ public static final Unit<MolarWeight> UNIT = new ProductUnit<MolarWeight>(\r
+ SI.KILOGRAM.divide(SI.MOLE));\r
+ }\r
+ \r
+\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim.fuel;\r
+\r
+import javax.measure.quantity.Pressure;\r
+import javax.measure.quantity.Temperature;\r
+import javax.measure.quantity.VolumetricDensity;\r
+import javax.measure.unit.SI;\r
+\r
+import org.jscience.physics.amount.Amount;\r
+\r
+import com.billkuker.rocketry.motorsim.RocketScience.MolarWeight;\r
+\r
+public class KNSU extends SaintRobertFuel {\r
+ \r
+ public KNSU(){\r
+ super(Type.English);\r
+ }\r
+\r
+ //@Override \r
+ public Amount<VolumetricDensity> idealDensity() {\r
+ //return Amount.valueOf(1.889, 0, SI.GRAM.divide(SI.CENTIMETER.pow(3))).to(VolumetricDensity.UNIT);\r
+ return Amount.valueOf(1889, 0, SI.KILOGRAM.divide(SI.METER.pow(3))).to(VolumetricDensity.UNIT);\r
+ }\r
+\r
+ @Override\r
+ protected double burnrateCoefficient(Amount<Pressure> pressure) {\r
+ return 0.0665;\r
+ }\r
+\r
+ @Override\r
+ protected double burnrateExponent(Amount<Pressure> pressure) {\r
+ return 0.319;\r
+ }\r
+ \r
+ public CombustionProduct getCombustionProduct(){\r
+ return new CombustionProduct(){\r
+ \r
+ @Override\r
+ public Amount<Temperature> idealCombustionTemperature() {\r
+ return Amount.valueOf(1720, SI.KELVIN);\r
+ }\r
+ \r
+ @Override\r
+ public Amount<MolarWeight> effectiveMolarWeight() {\r
+ return Amount.valueOf("41.98 kg/kmol").to(MolarWeight.UNIT);\r
+ }\r
+ \r
+ @Override\r
+ public double ratioOfSpecificHeats() {\r
+ return 1.133;\r
+ }\r
+ \r
+ @Override\r
+ public double ratioOfSpecificHeats2Phase() {\r
+ return 1.044;\r
+ }\r
+ };\r
+ }\r
+\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim.fuel;\r
+\r
+import javax.measure.quantity.Pressure;\r
+import javax.measure.quantity.Velocity;\r
+import javax.measure.quantity.VolumetricDensity;\r
+import javax.measure.unit.NonSI;\r
+import javax.measure.unit.SI;\r
+import javax.measure.unit.Unit;\r
+\r
+import org.jscience.physics.amount.Amount;\r
+\r
+import com.billkuker.rocketry.motorsim.Fuel;\r
+\r
+public abstract class SaintRobertFuel implements Fuel {\r
+ \r
+ protected enum Type{\r
+ Si(SI.MILLIMETER.divide(SI.SECOND), SI.MEGA(SI.PASCAL)),\r
+ English(NonSI.INCH.divide(SI.SECOND), NonSI.POUND_FORCE.divide(NonSI.INCH.pow(2)))\r
+ ;\r
+ \r
+ private final Unit<Velocity> v;\r
+ private final Unit<Pressure> p;\r
+ \r
+ Type( Unit v, Unit p){\r
+ this.p = p;\r
+ this.v = v;\r
+ }\r
+ }\r
+ \r
+ private Type t = Type.Si;\r
+ \r
+ public SaintRobertFuel(Type t){\r
+ this.t = t;\r
+ }\r
+\r
+ @Override\r
+ public Amount<Velocity> burnRate(Amount<Pressure> pressure) {\r
+\r
+ \r
+ double p = pressure.doubleValue(t.p);\r
+ double a = burnrateCoefficient(pressure);\r
+ double n = burnrateExponent(pressure);\r
+ \r
+ return burnrateConstant().plus(Amount.valueOf( a*Math.pow(p,n), t.v ));\r
+ }\r
+ \r
+ protected abstract double burnrateCoefficient(Amount<Pressure> pressure);\r
+ \r
+ protected abstract double burnrateExponent(Amount<Pressure> pressure);\r
+ \r
+ protected Amount<Velocity> burnrateConstant(){\r
+ return Amount.valueOf(0, Velocity.UNIT);\r
+ }\r
+\r
+ @Override\r
+ public abstract Amount<VolumetricDensity> idealDensity();\r
+\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim.grain;\r
+\r
+import javax.measure.quantity.Area;\r
+import javax.measure.quantity.Length;\r
+import javax.measure.quantity.Volume;\r
+import javax.measure.unit.SI;\r
+\r
+import org.jscience.physics.amount.Amount;\r
+\r
+import com.billkuker.rocketry.motorsim.Grain;\r
+import com.billkuker.rocketry.motorsim.validation.Validating;\r
+import com.billkuker.rocketry.motorsim.validation.ValidationException;\r
+\r
+public class CoredCylindricalGrain implements Grain, Validating {\r
+\r
+ private Amount<Length> length, oD, iD;\r
+ private boolean oInh = true, iInh = false, eInh = false;\r
+\r
+ public CoredCylindricalGrain() {\r
+\r
+ }\r
+\r
+ @Override\r
+ public Amount<Area> surfaceArea(Amount<Length> regression) {\r
+ Amount<Length> zero = Amount.valueOf(0, SI.MILLIMETER);\r
+ \r
+ //Calculated regressed length\r
+ Amount<Length> cLength = length;\r
+ if ( !eInh ){\r
+ cLength = cLength.minus(regression.times(2));\r
+ }\r
+ \r
+ //Calculate regressed iD\r
+ Amount<Length> cID = iD;\r
+ if ( !iInh ){\r
+ cID = iD.plus(regression.times(2));\r
+ }\r
+ \r
+ //Calculate regressed oD\r
+ Amount<Length> cOD = oD;\r
+ if ( !oInh ){\r
+ cOD = oD.minus(regression.times(2));\r
+ }\r
+ \r
+ if ( cID.isGreaterThan(cOD) )\r
+ return Amount.valueOf(0, SI.SQUARE_METRE);\r
+ if ( cOD.isLessThan(cID) )\r
+ return Amount.valueOf(0, SI.SQUARE_METRE);\r
+ if ( cLength.isLessThan(zero) )\r
+ return Amount.valueOf(0, SI.SQUARE_METRE);\r
+ \r
+ Amount<Area> inner = cID.times(Math.PI).times(cLength).to(SI.SQUARE_METRE);\r
+ \r
+ Amount<Area> outer = cOD.times(Math.PI).times(cLength).to(SI.SQUARE_METRE);\r
+ \r
+ Amount<Area> ends = (cOD.divide(2).pow(2).times(Math.PI)).minus(cID.divide(2).pow(2).times(Math.PI)).times(2).to(SI.SQUARE_METRE);\r
+ \r
+ Amount<Area> total = inner.times(iInh?0:1).plus(outer.times(oInh?0:1)).plus(ends.times(eInh?0:1));\r
+ \r
+ return total;\r
+ }\r
+\r
+ @Override\r
+ public Amount<Volume> volume(Amount<Length> regression) {\r
+ Amount<Length> zero = Amount.valueOf(0, SI.MILLIMETER);\r
+ \r
+ //Calculated regressed length\r
+ Amount<Length> cLength = length;\r
+ if ( !eInh ){\r
+ cLength = cLength.minus(regression.times(2));\r
+ }\r
+ \r
+ //Calculate regressed iD\r
+ Amount<Length> cID = iD;\r
+ if ( !iInh ){\r
+ cID = iD.plus(regression.times(2));\r
+ }\r
+ \r
+ //Calculate regressed oD\r
+ Amount<Length> cOD = oD;\r
+ if ( !oInh ){\r
+ cOD = oD.minus(regression.times(2));\r
+ }\r
+ \r
+ if ( cID.isGreaterThan(cOD) )\r
+ return Amount.valueOf(0, SI.CUBIC_METRE);\r
+ if ( cOD.isLessThan(cID) )\r
+ return Amount.valueOf(0, SI.CUBIC_METRE);\r
+ if ( cLength.isLessThan(zero) )\r
+ return Amount.valueOf(0, SI.CUBIC_METRE);\r
+ \r
+ \r
+ Amount<Area> end = (cOD.divide(2).pow(2).times(Math.PI)).minus(cID.divide(2).pow(2).times(Math.PI)).to(SI.SQUARE_METRE);\r
+\r
+ return end.times(cLength).to(SI.CUBIC_METRE);\r
+ }\r
+\r
+ public void setLength(Amount<Length> length) {\r
+ this.length = length;\r
+ }\r
+\r
+ public void setOD(Amount<Length> od) {\r
+ oD = od;\r
+ }\r
+\r
+ public void setID(Amount<Length> id) {\r
+ iD = id;\r
+ }\r
+ \r
+ public void checkValidity() throws ValidationException{\r
+ if ( iD.equals(Amount.ZERO) )\r
+ throw new ValidationException(this, "Invalid iD");\r
+ if ( oD.equals(Amount.ZERO) )\r
+ throw new ValidationException(this, "Invalid oD");\r
+ if ( length.equals(Amount.ZERO) )\r
+ throw new ValidationException(this, "Invalid Length");\r
+ if ( iD.isGreaterThan(oD) )\r
+ throw new ValidationException(this, "iD > oD");\r
+ \r
+ if ( iInh && oInh && eInh )\r
+ throw new ValidationException(this, "No exposed grain surface");\r
+ \r
+ }\r
+\r
+ @Override\r
+ public Amount<Length> webThickness() {\r
+ Amount<Length> radial = null;\r
+ if ( !iInh && !oInh )\r
+ radial = oD.minus(iD).divide(4); //Outer and inner exposed\r
+ else if ( !iInh || !oInh )\r
+ radial = oD.minus(iD).divide(2); //Outer or inner exposed\r
+ \r
+ Amount<Length> axial = null;\r
+ \r
+ if ( !eInh )\r
+ axial = length.divide(2);\r
+ \r
+ if ( axial == null )\r
+ return radial;\r
+ if ( radial == null )\r
+ return axial;\r
+ if ( radial.isLessThan(axial) )\r
+ return radial;\r
+ return axial;\r
+ }\r
+\r
+ public Amount<Length> getLength() {\r
+ return length;\r
+ }\r
+\r
+ public Amount<Length> getOD() {\r
+ return oD;\r
+ }\r
+\r
+ public Amount<Length> getID() {\r
+ return iD;\r
+ }\r
+ \r
+ \r
+ \r
+\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim.grain;\r
+\r
+import java.awt.BasicStroke;\r
+import java.awt.BorderLayout;\r
+import java.awt.Color;\r
+import java.awt.Dimension;\r
+import java.awt.Graphics;\r
+import java.awt.Graphics2D;\r
+import java.awt.Rectangle;\r
+import java.awt.Shape;\r
+import java.awt.geom.AffineTransform;\r
+import java.awt.geom.Ellipse2D;\r
+import java.awt.geom.PathIterator;\r
+import java.awt.geom.Rectangle2D;\r
+import java.util.HashSet;\r
+import java.util.NoSuchElementException;\r
+import java.util.Set;\r
+import java.util.SortedMap;\r
+import java.util.TreeMap;\r
+\r
+import javax.measure.quantity.Area;\r
+import javax.measure.quantity.Dimensionless;\r
+import javax.measure.quantity.Length;\r
+import javax.measure.quantity.Volume;\r
+import javax.measure.unit.SI;\r
+import javax.swing.JFrame;\r
+import javax.swing.JLabel;\r
+import javax.swing.JPanel;\r
+import javax.swing.JSlider;\r
+import javax.swing.JSplitPane;\r
+import javax.swing.event.ChangeEvent;\r
+import javax.swing.event.ChangeListener;\r
+\r
+import org.jscience.physics.amount.Amount;\r
+\r
+import com.billkuker.rocketry.motorsim.Grain;\r
+import com.billkuker.rocketry.motorsim.visual.Chart;\r
+\r
+public class ExtrudedGrain implements Grain, Grain.Graphical {\r
+ \r
+ Set<Shape> plus = new HashSet<Shape>();\r
+ Set<Shape> minus = new HashSet<Shape>();\r
+ Set<Shape> inhibited = new HashSet<Shape>();\r
+ \r
+ Amount<Length> length = Amount.valueOf(25, SI.MILLIMETER);\r
+ \r
+ Amount<Length> rStep;\r
+ \r
+ private class RegEntry{\r
+ Amount<Area> surfaceArea;\r
+ Amount<Volume> volume;\r
+ }\r
+ \r
+ SortedMap<Amount<Length>, RegEntry> data = new TreeMap<Amount<Length>, RegEntry>();\r
+ \r
+ Amount<Length> webThickness;\r
+ \r
+ {\r
+ /* Similar test grain*/\r
+ Shape outside = new Ellipse2D.Double(50,50,30,30);\r
+ plus.add(outside);\r
+ minus.add(new Ellipse2D.Double(50,60,10,10));\r
+ inhibited.add(outside);\r
+ length = Amount.valueOf(70, SI.MILLIMETER);\r
+ /*/\r
+ \r
+ /*Big c-slot\r
+ Shape outside = new Ellipse2D.Double(0,0,30,30);\r
+ plus.add(outside);\r
+ inhibited.add(outside);\r
+ minus.add(new Rectangle2D.Double(12,12,6,20));\r
+ length = Amount.valueOf(70, SI.MILLIMETER);\r
+ */\r
+ \r
+ /*Plus sign\r
+ Shape outside = new Ellipse2D.Double(0,0,200,200);\r
+ plus.add(outside);\r
+ inhibited.add(outside);\r
+ minus.add(new Rectangle2D.Double(90,40,20,120));\r
+ minus.add(new Rectangle2D.Double(40,90,120,20));\r
+ */\r
+ \r
+ findWebThickness();\r
+ \r
+ fillInData();\r
+ }\r
+ \r
+ @Override\r
+ public Amount<Area> surfaceArea(Amount<Length> regression) {\r
+ if ( regression.isGreaterThan(webThickness) )\r
+ return Amount.valueOf(0, Area.UNIT);\r
+ Amount<Length> highKey = data.tailMap(regression).firstKey();\r
+ Amount<Length> lowKey;\r
+ try{\r
+ lowKey = data.headMap(regression).lastKey();\r
+ } catch ( NoSuchElementException e ){\r
+ return data.get(highKey).surfaceArea;\r
+ }\r
+ \r
+ double lp = regression.minus(lowKey).divide(highKey.minus(lowKey)).to(Dimensionless.UNIT).doubleValue(Dimensionless.UNIT);\r
+ \r
+ Amount<Area> lowVal = data.get(lowKey).surfaceArea;\r
+ Amount<Area> highVal = data.get(highKey).surfaceArea;\r
+ \r
+ return lowVal.times(1-lp).plus(highVal.times(lp));\r
+ }\r
+ \r
+ @Override\r
+ public Amount<Volume> volume(Amount<Length> regression) {\r
+ if ( regression.isGreaterThan(webThickness) )\r
+ return Amount.valueOf(0, Volume.UNIT);\r
+ Amount<Length> highKey = data.tailMap(regression).firstKey();\r
+ Amount<Length> lowKey;\r
+ try{\r
+ lowKey = data.headMap(regression).lastKey();\r
+ } catch ( NoSuchElementException e ){\r
+ return data.get(highKey).volume;\r
+ }\r
+ \r
+ double lp = regression.minus(lowKey).divide(highKey.minus(lowKey)).to(Dimensionless.UNIT).doubleValue(Dimensionless.UNIT);\r
+ \r
+ Amount<Volume> lowVal = data.get(lowKey).volume;\r
+ Amount<Volume> highVal = data.get(highKey).volume;\r
+ \r
+ return lowVal.times(1-lp).plus(highVal.times(lp));\r
+ }\r
+ \r
+ private void fillInData(){\r
+ double max = webThickness().doubleValue(SI.MILLIMETER);\r
+ double delta = max / 5;\r
+ rStep = Amount.valueOf(delta, SI.MILLIMETER).divide(10);\r
+ for( double r = 0; r <= max+1; r += delta){\r
+ RegEntry e = new RegEntry();\r
+ Amount<Length> regression = Amount.valueOf(r, SI.MILLIMETER);\r
+\r
+ \r
+ Amount<Length> rLen = length.minus(regression.times(2));\r
+ if ( rLen.isLessThan(Amount.valueOf(0, SI.MILLIMETER)))\r
+ break;\r
+ \r
+ System.out.println("Calculating area for regression " + regression);\r
+ \r
+ java.awt.geom.Area burn = getArea(regression);\r
+ if ( burn.isEmpty() )\r
+ break;\r
+ burn.subtract(getArea(regression.plus(Amount.valueOf(.001, SI.MILLIMETER))));\r
+ \r
+ Amount<Area> xSection = crossSectionArea(getArea(regression));\r
+ \r
+ e.volume = xSection.times(rLen).to(Volume.UNIT);\r
+ \r
+ e.surfaceArea = perimeter(burn).divide(2).times(rLen).plus(xSection.times(2)).to(Area.UNIT);\r
+ \r
+ data.put(regression, e);\r
+\r
+ }\r
+ \r
+ RegEntry e = new RegEntry();\r
+ e.surfaceArea = Amount.valueOf(0, Area.UNIT);\r
+ e.volume = Amount.valueOf(0, Volume.UNIT);\r
+ data.put( webThickness(), e );\r
+ }\r
+ \r
+ private Amount<Area> crossSectionArea(java.awt.geom.Area a ) {\r
+ Rectangle r = a.getBounds();\r
+ int cnt = 0;\r
+ int total = 0;\r
+ double e = .1;\r
+ for( double x = r.getMinX(); x < r.getMaxX(); x += e){\r
+ for( double y = r.getMinY(); y < r.getMaxY(); y += e){\r
+ if ( a.contains(x, y) )\r
+ cnt++;\r
+ total++;\r
+ }\r
+ }\r
+ System.out.println(cnt + " out of " + total);\r
+ return Amount.valueOf(r.getWidth() * r.getHeight() * cnt / total, SI.MILLIMETER.pow(2)).to(Area.UNIT);\r
+\r
+ }\r
+ \r
+ \r
+\r
+\r
+\r
+ @Override\r
+ public Amount<Length> webThickness() {\r
+ return webThickness;\r
+ }\r
+ \r
+ private Amount<Length> perimeter(java.awt.geom.Area a){\r
+ PathIterator i = a.getPathIterator(new AffineTransform(), .001);\r
+ double x=0, y=0;\r
+ double len = 0;\r
+ while ( !i.isDone() ){\r
+ double coords[] = new double[6];\r
+ int type = i.currentSegment(coords);\r
+ if ( type == PathIterator.SEG_LINETO ){\r
+ //System.out.println("Line");\r
+ double nx = coords[0];\r
+ double ny = coords[1];\r
+ //System.out.println(x+","+y+ " to " + nx+"," + ny);\r
+ len += Math.sqrt(Math.pow(x-nx, 2) + Math.pow(y-ny,2));\r
+ x = nx;\r
+ y = ny;\r
+ } else if ( type == PathIterator.SEG_MOVETO ){\r
+ //System.out.println("Move");\r
+ x = coords[0];\r
+ y = coords[1];\r
+ } else {\r
+ //System.err.println("Got " + type);\r
+ }\r
+ i.next();\r
+ }\r
+ return Amount.valueOf(len, SI.MILLIMETER);\r
+ }\r
+ \r
+ private void findWebThickness(){\r
+ java.awt.geom.Area a = getArea(Amount.valueOf(0, SI.MILLIMETER));\r
+ Rectangle r = a.getBounds();\r
+ double max = r.getWidth()<r.getHeight()?r.getHeight():r.getWidth(); //The max size\r
+ double min = 0;\r
+ double guess;\r
+ while(true){\r
+ guess = min + (max-min)/2; //Guess halfway through\r
+ System.out.println("Min: " + min + " Guess: " + guess + " Max: " + max);\r
+ a = getArea(Amount.valueOf(guess, SI.MILLIMETER));\r
+ if ( a.isEmpty() ){\r
+ //guess is too big\r
+ max = guess;\r
+ } else {\r
+ //min is too big\r
+ min = guess;\r
+ }\r
+ if ( (max-min) < .01 )\r
+ break;\r
+ }\r
+ webThickness = Amount.valueOf(guess, SI.MILLIMETER);\r
+ if ( webThickness.isGreaterThan(length.divide(2)))\r
+ webThickness = length.divide(2);\r
+ }\r
+ private java.awt.geom.Area getArea(Amount<Length> regression){\r
+ java.awt.geom.Area a = new java.awt.geom.Area();\r
+ for ( Shape s: plus )\r
+ a.add(new java.awt.geom.Area(regress(s, regression.doubleValue(SI.MILLIMETER), true)));\r
+ for ( Shape s: minus )\r
+ a.subtract(new java.awt.geom.Area(regress(s, regression.doubleValue(SI.MILLIMETER), false)));\r
+ return a;\r
+ }\r
+ \r
+ private Shape regress(Shape s, double mm, boolean plus){\r
+ if ( inhibited.contains(s) )\r
+ return s;\r
+ if ( s instanceof Ellipse2D ){\r
+ Ellipse2D e = (Ellipse2D)s;\r
+ \r
+ double d = plus?-2*mm:2*mm;\r
+ \r
+ double w = e.getWidth() + d;\r
+ double h = e.getHeight() + d;\r
+ double x = e.getX() - d/2;\r
+ double y = e.getY() - d/2;\r
+ \r
+ return new Ellipse2D.Double(x,y,w,h);\r
+ } else if ( s instanceof Rectangle2D ){\r
+ Rectangle2D r = (Rectangle2D)s;\r
+ \r
+ double d = plus?-2*mm:2*mm;\r
+ \r
+ double w = r.getWidth() + d;\r
+ double h = r.getHeight() + d;\r
+ double x = r.getX() - d/2;\r
+ double y = r.getY() - d/2;\r
+ \r
+ return new Rectangle2D.Double(x,y,w,h);\r
+ }\r
+ return null;\r
+ }\r
+ \r
+\r
+ public static void main(String args[]) throws Exception {\r
+ ExtrudedGrain e = new ExtrudedGrain();\r
+ new GrainPanel(e).show();\r
+ \r
+ }\r
+\r
+ @Override\r
+ public void draw(Graphics2D g2d, Amount<Length> regression) {\r
+ \r
+ java.awt.geom.Area reg = getArea(regression);\r
+ java.awt.geom.Area burn = getArea(regression);\r
+ burn.subtract(getArea(regression.plus(Amount.valueOf(.001, SI.MILLIMETER))));\r
+ java.awt.geom.Area noreg = getArea(Amount.valueOf(0,SI.MILLIMETER));\r
+ \r
+ Rectangle bounds = noreg.getBounds();\r
+ g2d.scale(200/bounds.getWidth(), 200/bounds.getHeight());\r
+ g2d.translate(-bounds.getX(), -bounds.getY());\r
+ \r
+\r
+ g2d.setStroke(new BasicStroke(0.5f));\r
+ \r
+ g2d.setColor(Color.GRAY);\r
+ g2d.fill(reg);\r
+ g2d.setColor(Color.RED);\r
+ g2d.draw(burn);\r
+ g2d.setColor(Color.BLACK);\r
+ g2d.draw(noreg);\r
+ \r
+ }\r
+\r
+\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim.grain;\r
+\r
+import java.awt.BorderLayout;\r
+import java.awt.Dimension;\r
+import java.awt.Graphics;\r
+import java.awt.Graphics2D;\r
+\r
+import javax.measure.quantity.Area;\r
+import javax.measure.quantity.Length;\r
+import javax.measure.quantity.Volume;\r
+import javax.measure.unit.SI;\r
+import javax.swing.JFrame;\r
+import javax.swing.JLabel;\r
+import javax.swing.JPanel;\r
+import javax.swing.JSlider;\r
+import javax.swing.JSplitPane;\r
+import javax.swing.event.ChangeEvent;\r
+import javax.swing.event.ChangeListener;\r
+\r
+import org.jscience.physics.amount.Amount;\r
+\r
+import com.billkuker.rocketry.motorsim.Grain;\r
+import com.billkuker.rocketry.motorsim.visual.Chart;\r
+\r
+public class GrainPanel extends JPanel {\r
+ private static final long serialVersionUID = 1L;\r
+ private Amount<Length> displayedRegression = Amount.valueOf(0, SI.MILLIMETER);\r
+ private JLabel l = new JLabel();\r
+ private Chart<Length,Area> area;\r
+ Chart<Length, Volume> volume;\r
+ private XC xc;\r
+ private SL sl;\r
+ private Grain grain;\r
+ \r
+ public GrainPanel(Grain g){\r
+ super(new BorderLayout());\r
+\r
+ grain = g;\r
+\r
+ try {\r
+\r
+ area = new Chart<Length, Area>(\r
+ SI.MILLIMETER,\r
+ SI.MILLIMETER.pow(2).asType(Area.class),\r
+ grain,\r
+ "surfaceArea");\r
+ area.setDomain(area.new IntervalDomain(Amount.valueOf(0, SI.MILLIMETER), grain.webThickness()));\r
+ \r
+ volume = new Chart<Length, Volume>(\r
+ SI.MILLIMETER,\r
+ SI.MILLIMETER.pow(3).asType(Volume.class),\r
+ grain,\r
+ "volume");\r
+ volume.setDomain(volume.new IntervalDomain(Amount.valueOf(0, SI.MILLIMETER), grain.webThickness()));\r
+\r
+ area.setMaximumSize(new Dimension(200,100));\r
+ volume.setMaximumSize(new Dimension(200,100));\r
+ \r
+\r
+ } catch (ClassCastException e) {\r
+ // TODO Auto-generated catch block\r
+ e.printStackTrace();\r
+ } catch (NoSuchMethodException e) {\r
+ // TODO Auto-generated catch block\r
+ e.printStackTrace();\r
+ }\r
+ \r
+ \r
+ JSplitPane charts = new JSplitPane(JSplitPane.VERTICAL_SPLIT, area, volume);\r
+ charts.setDividerLocation(.5);\r
+ \r
+ \r
+ if ( grain instanceof Grain.Graphical)\r
+ add(xc = new XC((Grain.Graphical)grain), BorderLayout.CENTER);\r
+ \r
+ JPanel left = new JPanel(new BorderLayout());\r
+ left.add(xc);\r
+ left.add(l, BorderLayout.NORTH);\r
+ left.add( new SL(), BorderLayout.SOUTH);\r
+ \r
+ add(new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, left, charts));\r
+\r
+ }\r
+ \r
+ private class XC extends JPanel{\r
+ private static final long serialVersionUID = 1L;\r
+ Grain.Graphical grain;\r
+ public XC(Grain.Graphical g){\r
+ setMinimumSize(new Dimension(120,120));\r
+ grain = g;\r
+ }\r
+ public void paint(Graphics g){\r
+ super.paint(g);\r
+ Graphics2D g2d = (Graphics2D)g;\r
+ g2d.translate(10, 30);\r
+ grain.draw(g2d, displayedRegression );\r
+ }\r
+ }\r
+ \r
+ private class SL extends JSlider implements ChangeListener{\r
+ private static final long serialVersionUID = 1L;\r
+ private static final int STEPS = 20;\r
+ public SL(){\r
+ addChangeListener(this);\r
+ setMinimum(0);\r
+ setMaximum(STEPS);\r
+ setValue(0);\r
+ }\r
+ \r
+ @Override\r
+ public void stateChanged(ChangeEvent e) {\r
+ double r = ((SL)e.getSource()).getValue();\r
+ displayedRegression = grain.webThickness().divide(STEPS).times(r);\r
+ l.setText("Regression: " + displayedRegression);\r
+ area.mark(displayedRegression);\r
+ volume.mark(displayedRegression);\r
+ if ( xc != null )\r
+ xc.repaint();\r
+ }\r
+ }\r
+ \r
+ public void show(){\r
+ JFrame f = new JFrame();\r
+ f.setSize(1024,600);\r
+ f.setContentPane(this);\r
+ f.setDefaultCloseOperation(f.DISPOSE_ON_CLOSE);\r
+ f.setVisible(true);\r
+ }\r
+\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim.test;\r
+\r
+import javax.measure.quantity.Area;\r
+import javax.measure.quantity.Force;\r
+import javax.measure.quantity.Pressure;\r
+import javax.measure.unit.SI;\r
+\r
+import junit.framework.Assert;\r
+\r
+import org.jscience.physics.amount.Amount;\r
+import org.junit.Test;\r
+\r
+import com.billkuker.rocketry.motorsim.ConvergentDivergentNozzle;\r
+import com.billkuker.rocketry.motorsim.fuel.KNSU;\r
+\r
+public class ConvergentDivergentNozzleTest extends RocketTest {\r
+ \r
+ ConvergentDivergentNozzle n = new ConvergentDivergentNozzle();\r
+ \r
+ {\r
+ n.setThroatDiameter(Amount.valueOf(6.6, SI.MILLIMETER));\r
+ n.setExitDiameter(Amount.valueOf(20.87, SI.MILLIMETER));\r
+ n.setEfficiency(0.85);\r
+ }\r
+\r
+ @Test\r
+ public void testThroatArea() {\r
+ Amount<Area> a = n.throatArea().to(SI.MILLIMETER.pow(2).asType(Area.class));\r
+ assertApproximate(Amount.valueOf(34.2, .1, SI.MILLIMETER.pow(2)), a);\r
+ }\r
+\r
+ @Test\r
+ public void testExitArea() {\r
+ Amount<Area> a = n.exitArea();\r
+ assertApproximate(Amount.valueOf(342, .1, SI.MILLIMETER.pow(2)), a);\r
+ }\r
+\r
+ @Test\r
+ public void testThrust() {\r
+ Amount<Pressure> Patm = Amount.valueOf(101000, SI.PASCAL);\r
+ Amount<Pressure> Po = Amount.valueOf(2046491, SI.PASCAL);\r
+ Amount<Pressure> Pe = Patm;\r
+\r
+ KNSU f = new KNSU();\r
+ \r
+ Amount<Force> t = n.thrust(Po, Pe, Patm, f.getCombustionProduct().ratioOfSpecificHeats2Phase());\r
+ \r
+ Amount<Force> expected = Amount.valueOf(87.2, .1, SI.NEWTON);\r
+ \r
+ assertApproximate(t, expected);\r
+ \r
+ System.out.println(t);\r
+ }\r
+\r
+ @Test\r
+ public void testThrustCoefficient() {\r
+ Amount<Pressure> Patm = Amount.valueOf(101000, SI.PASCAL);\r
+ Amount<Pressure> Po = Amount.valueOf(2046491, SI.PASCAL);\r
+ Amount<Pressure> Pe = Patm;\r
+\r
+ KNSU f = new KNSU();\r
+ \r
+ double cF = n.thrustCoefficient(Po, Pe, Patm, f.getCombustionProduct().ratioOfSpecificHeats2Phase());\r
+ \r
+ \r
+ Assert.assertEquals(cF, 1.2454812344324655);\r
+ }\r
+\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim.test;\r
+\r
+import javax.measure.quantity.Length;\r
+import javax.measure.unit.SI;\r
+\r
+import org.jscience.physics.amount.Amount;\r
+import org.junit.Test;\r
+\r
+import com.billkuker.rocketry.motorsim.grain.CoredCylindricalGrain;\r
+import com.billkuker.rocketry.motorsim.validation.ValidationException;\r
+\r
+public class CoredCylindricalGrainTest extends RocketTest {\r
+\r
+ @Test\r
+ public void testSurfaceArea() {\r
+ CoredCylindricalGrain g = new CoredCylindricalGrain();\r
+ \r
+ g.setLength(Amount.valueOf(70, SI.MILLIMETER));\r
+ \r
+ g.setOD(Amount.valueOf(23.5, SI.MILLIMETER));\r
+ \r
+ g.setID(Amount.valueOf(7.9375, SI.MILLIMETER));\r
+ \r
+ for ( int mm = 0; mm < 15; mm++ ){\r
+ Amount<Length> r = Amount.valueOf(mm, SI.MILLIMETER);\r
+ System.out.println( r + ", " + g.surfaceArea(r).to(SI.MILLIMETER.pow(2)) + ", " + g.volume(r) );\r
+ }\r
+ }\r
+ \r
+\r
+ @Test\r
+ public void testWebThickness() {\r
+ CoredCylindricalGrain g = new CoredCylindricalGrain();\r
+ \r
+ //thin and long\r
+ g.setLength(Amount.valueOf(100, SI.MILLIMETER));\r
+ g.setOD(Amount.valueOf(30, SI.MILLIMETER));\r
+ g.setID(Amount.valueOf(10, SI.MILLIMETER));\r
+ \r
+ assertApproximate(g.webThickness(), Amount.valueOf("10mm"));\r
+ \r
+ //thick and short\r
+ g.setLength(Amount.valueOf(100, SI.MILLIMETER));\r
+ g.setOD(Amount.valueOf(300, SI.MILLIMETER));\r
+ g.setID(Amount.valueOf(100, SI.MILLIMETER));\r
+ \r
+ assertApproximate(g.webThickness(), Amount.valueOf("50mm"));\r
+ }\r
+\r
+ @Test\r
+ public void testVolume() {\r
+ CoredCylindricalGrain g = new CoredCylindricalGrain();\r
+ \r
+ //thin and long\r
+ g.setLength(Amount.valueOf(100, SI.MILLIMETER));\r
+ g.setOD(Amount.valueOf(30, SI.MILLIMETER));\r
+ g.setID(Amount.valueOf(10, SI.MILLIMETER));\r
+ \r
+ System.out.println(g.volume(Amount.valueOf(0, SI.MILLIMETER)));\r
+ }\r
+\r
+ @Test(expected=ValidationException.class)\r
+ public void testCheckValidity() throws ValidationException{\r
+ CoredCylindricalGrain g = new CoredCylindricalGrain();\r
+ \r
+ //thin and long\r
+ g.setLength(Amount.valueOf(100, SI.MILLIMETER));\r
+ g.setOD(Amount.valueOf(10, SI.MILLIMETER));\r
+ g.setID(Amount.valueOf(30, SI.MILLIMETER));\r
+ \r
+ g.checkValidity();\r
+ }\r
+\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim.test;\r
+\r
+import javax.measure.unit.SI;\r
+\r
+import org.jscience.physics.amount.Amount;\r
+import org.junit.Assert;\r
+import org.junit.Test;\r
+\r
+import com.billkuker.rocketry.motorsim.CylindricalChamber;\r
+\r
+public class CylindricalChamberTest extends RocketTest {\r
+\r
+ @Test\r
+ public void testBurstPressure() {\r
+ Assert.assertNull( new CylindricalChamber().burstPressure() );\r
+ }\r
+\r
+ @Test\r
+ public void testChamberVolume() {\r
+ CylindricalChamber c = new CylindricalChamber();\r
+ \r
+ c.setLength(Amount.valueOf(100, SI.MILLIMETER));\r
+ c.setID(Amount.valueOf(20, SI.MILLIMETER));\r
+ \r
+ assertApproximate(c.chamberVolume(), Amount.valueOf(10000*Math.PI, SI.MILLIMETER.pow(3)));\r
+ }\r
+\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim.test;\r
+\r
+import javax.measure.unit.NonSI;\r
+import javax.measure.unit.SI;\r
+\r
+import org.jscience.physics.amount.Amount;\r
+import org.junit.Test;\r
+\r
+import com.billkuker.rocketry.motorsim.fuel.KNSU;\r
+\r
+public class KNSUTest extends RocketTest {\r
+\r
+ @Test\r
+ public void testBurnRate() {\r
+ KNSU f = new KNSU();\r
+ \r
+ System.out.println(f.burnRate(Amount.valueOf(6.89, SI.MEGA(SI.PASCAL) )).to(NonSI.INCH.divide(SI.SECOND)));\r
+ }\r
+\r
+ @Test\r
+ public void testEffectiveMolarWeight(){\r
+ (new KNSU()).getCombustionProduct().effectiveMolarWeight();\r
+ }\r
+ \r
+ @Test\r
+ public void testIdealDensity(){\r
+ System.out.println((new KNSU()).idealDensity());\r
+ System.out.println((new KNSU()).idealDensity().isExact());\r
+ }\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim.test;\r
+\r
+import org.jscience.physics.amount.Amount;\r
+import org.junit.Assert;\r
+\r
+public class RocketTest {\r
+\r
+ protected void assertApproximate(Amount a, Amount b){\r
+ Assert.assertTrue("" + a + " !~ " + b , a.approximates(b));\r
+ }\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim.validation;\r
+\r
+public interface Validating {\r
+ void checkValidity() throws ValidationException;\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim.validation;\r
+\r
+public class ValidationException extends Exception {\r
+ /**\r
+ * \r
+ */\r
+ private static final long serialVersionUID = 1L;\r
+\r
+ public ValidationException(Validating v, String error) {\r
+ super(v.toString() + ": " + error);\r
+ }\r
+}\r
--- /dev/null
+package com.billkuker.rocketry.motorsim.visual;\r
+import java.awt.BorderLayout;\r
+import java.awt.Color;\r
+import java.lang.reflect.InvocationTargetException;\r
+import java.lang.reflect.Method;\r
+import java.util.Iterator;\r
+\r
+import javax.measure.quantity.Area;\r
+import javax.measure.quantity.Length;\r
+import javax.measure.quantity.Quantity;\r
+import javax.measure.quantity.Volume;\r
+import javax.measure.unit.SI;\r
+import javax.measure.unit.Unit;\r
+import javax.swing.JFrame;\r
+import javax.swing.JPanel;\r
+\r
+import org.jfree.chart.ChartFactory;\r
+import org.jfree.chart.ChartPanel;\r
+import org.jfree.chart.JFreeChart;\r
+import org.jfree.chart.plot.Marker;\r
+import org.jfree.chart.plot.PlotOrientation;\r
+import org.jfree.chart.plot.ValueMarker;\r
+import org.jfree.data.xy.XYSeries;\r
+import org.jfree.data.xy.XYSeriesCollection;\r
+import org.jscience.physics.amount.Amount;\r
+\r
+import com.billkuker.rocketry.motorsim.grain.CoredCylindricalGrain;\r
+\r
+public class Chart<X extends Quantity, Y extends Quantity> extends JPanel {\r
+\r
+ \r
+ public class IntervalDomain implements Iterable<Amount<X>>{\r
+ \r
+ Amount<X> low, high, delta;\r
+ int steps = 100;\r
+\r
+ public IntervalDomain(Amount<X> low, Amount<X> high) {\r
+ this.low = low;\r
+ this.high = high;\r
+ delta = high.minus(low).divide(steps);\r
+ }\r
+\r
+ public IntervalDomain(Amount<X> low, Amount<X> high, int steps) {\r
+ this.steps = steps;\r
+ this.low = low;\r
+ this.high = high;\r
+ delta = high.minus(low).divide(steps);\r
+ }\r
+\r
+\r
+ @Override\r
+ public Iterator<Amount<X>> iterator() {\r
+ return new Iterator<Amount<X>>(){\r
+ Amount<X> current = low;\r
+ \r
+ @Override\r
+ public boolean hasNext() {\r
+ return current.isLessThan(high.plus(delta));\r
+ }\r
+\r
+ @Override\r
+ public Amount<X> next() {\r
+ current = current.plus(delta);\r
+ return current;\r
+ }\r
+ @Override\r
+ public final void remove() {\r
+ throw new UnsupportedOperationException("Chart domain iterators are not modifiable.");\r
+ }\r
+ };\r
+ }\r
+ \r
+ }\r
+ \r
+\r
+ XYSeries series;\r
+ XYSeriesCollection dataset = new XYSeriesCollection();\r
+ JFreeChart chart;\r
+\r
+ Unit<X> xUnit;\r
+ Unit<Y> yUnit;\r
+\r
+ Object source;\r
+ Method f;\r
+\r
+\r
+ public Chart(Unit<X> xUnit, Unit<Y> yUnit, Object source, String method)\r
+ throws NoSuchMethodException {\r
+ super(new BorderLayout());\r
+ f = source.getClass().getMethod(method, Amount.class);\r
+\r
+ series = new XYSeries(f.getName());\r
+ this.source = source;\r
+\r
+ dataset.addSeries(series);\r
+\r
+ this.xUnit = xUnit;\r
+ this.yUnit = yUnit;\r
+ chart = ChartFactory.createXYLineChart(source.getClass()\r
+ .getSimpleName(), // Title\r
+ xUnit.toString(), // x-axis Label\r
+ yUnit.toString(), // y-axis Label\r
+ dataset,\r
+ PlotOrientation.VERTICAL, // Plot Orientation\r
+ true, // Show Legend\r
+ true, // Use tool tips\r
+ false // Configure chart to generate URLs?\r
+ );\r
+ add(new ChartPanel(chart));\r
+ }\r
+ \r
+ private Marker marker;\r
+ public void mark(Amount<X> m){\r
+ if ( marker != null )\r
+ chart.getXYPlot().removeDomainMarker(marker);\r
+ if ( m != null ){\r
+ marker = new ValueMarker(m.doubleValue(xUnit));\r
+ marker.setPaint(Color.blue);\r
+ marker.setAlpha(0.8f);\r
+ chart.getXYPlot().addDomainMarker(marker);\r
+ }\r
+ }\r
+\r
+ public void setDomain(Iterable<Amount<X>> d) {\r
+\r
+ //double low = d.low.doubleValue(xUnit);\r
+ //double high = d.high.doubleValue(xUnit);\r
+ //double step = (high - low) / 50;\r
+ //for (double x = low; x < high; x += step) {\r
+ //Amount<X> ax = Amount.valueOf(x, xUnit);\r
+ for( Amount<X> ax: d){\r
+ try {\r
+ Amount<Y> y = (Amount<Y>) f.invoke(source, ax);\r
+ add(ax, y);\r
+ } catch (IllegalArgumentException e) {\r
+ // TODO Auto-generated catch block\r
+ e.printStackTrace();\r
+ } catch (IllegalAccessException e) {\r
+ // TODO Auto-generated catch block\r
+ e.printStackTrace();\r
+ } catch (InvocationTargetException e) {\r
+ // TODO Auto-generated catch block\r
+ e.printStackTrace();\r
+ }\r
+ }\r
+ }\r
+\r
+ private void add(Amount<X> x, Amount<Y> y) {\r
+ series.add(x.doubleValue(xUnit), y.doubleValue(yUnit));\r
+ }\r
+ \r
+ public void show(){\r
+ new JFrame(){\r
+ {\r
+ setContentPane(Chart.this);\r
+ setSize(640,480);\r
+ setDefaultCloseOperation(DISPOSE_ON_CLOSE);\r
+ }\r
+ }.setVisible(true);\r
+ }\r
+\r
+ public static void main(String args[]) throws Exception {\r
+ CoredCylindricalGrain g = new CoredCylindricalGrain();\r
+ g.setLength(Amount.valueOf(70, SI.MILLIMETER));\r
+ g.setOD(Amount.valueOf(30, SI.MILLIMETER));\r
+ g.setID(Amount.valueOf(10, SI.MILLIMETER));\r
+\r
+ Chart<Length, Area> c = new Chart<Length, Area>(SI.MILLIMETER,\r
+ SI.MILLIMETER.pow(2).asType(Area.class), g, "surfaceArea");\r
+\r
+ c.setDomain(c.new IntervalDomain(Amount.valueOf(0, SI.CENTIMETER), g\r
+ .webThickness()));\r
+\r
+ c.show();\r
+ \r
+ Chart<Length, Volume> v = new Chart<Length, Volume>(SI.MILLIMETER,\r
+ SI.MILLIMETER.pow(3).asType(Volume.class), g, "volume");\r
+\r
+ v.setDomain(c.new IntervalDomain(Amount.valueOf(0, SI.CENTIMETER), g\r
+ .webThickness()));\r
+\r
+ v.show();\r
+ }\r
+\r
+}\r