Initial Checkin
authorBill Kuker <bkuker@billkuker.com>
Sat, 7 Mar 2009 03:16:19 +0000 (03:16 +0000)
committerBill Kuker <bkuker@billkuker.com>
Sat, 7 Mar 2009 03:16:19 +0000 (03:16 +0000)
30 files changed:
.classpath [new file with mode: 0644]
.project [new file with mode: 0644]
jcommon-1.0.15.jar [new file with mode: 0644]
jfreechart-1.0.12.jar [new file with mode: 0644]
jscience.jar [new file with mode: 0644]
log4j-1.2.15.jar [new file with mode: 0644]
log4j.properties [new file with mode: 0644]
src/Test.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/Burn.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/Chamber.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/ConvergentDivergentNozzle.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/CylindricalChamber.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/Fuel.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/Grain.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/Motor.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/Nozzle.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/RocketScience.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/fuel/KNSU.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/fuel/SaintRobertFuel.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/grain/CoredCylindricalGrain.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/grain/ExtrudedGrain.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/grain/GrainPanel.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/test/ConvergentDivergentNozzleTest.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/test/CoredCylindricalGrainTest.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/test/CylindricalChamberTest.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/test/KNSUTest.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/test/RocketTest.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/validation/Validating.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/validation/ValidationException.java [new file with mode: 0644]
src/com/billkuker/rocketry/motorsim/visual/Chart.java [new file with mode: 0644]

diff --git a/.classpath b/.classpath
new file mode 100644 (file)
index 0000000..dadcce8
--- /dev/null
@@ -0,0 +1,11 @@
+<?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
diff --git a/.project b/.project
new file mode 100644 (file)
index 0000000..5fc6d6f
--- /dev/null
+++ b/.project
@@ -0,0 +1,17 @@
+<?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
diff --git a/jcommon-1.0.15.jar b/jcommon-1.0.15.jar
new file mode 100644 (file)
index 0000000..d0dc26d
Binary files /dev/null and b/jcommon-1.0.15.jar differ
diff --git a/jfreechart-1.0.12.jar b/jfreechart-1.0.12.jar
new file mode 100644 (file)
index 0000000..73be90f
Binary files /dev/null and b/jfreechart-1.0.12.jar differ
diff --git a/jscience.jar b/jscience.jar
new file mode 100644 (file)
index 0000000..52be1f1
Binary files /dev/null and b/jscience.jar differ
diff --git a/log4j-1.2.15.jar b/log4j-1.2.15.jar
new file mode 100644 (file)
index 0000000..c930a6a
Binary files /dev/null and b/log4j-1.2.15.jar differ
diff --git a/log4j.properties b/log4j.properties
new file mode 100644 (file)
index 0000000..23a7afe
--- /dev/null
@@ -0,0 +1,5 @@
+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
diff --git a/src/Test.java b/src/Test.java
new file mode 100644 (file)
index 0000000..f46a3c7
--- /dev/null
@@ -0,0 +1,16 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/Burn.java b/src/com/billkuker/rocketry/motorsim/Burn.java
new file mode 100644 (file)
index 0000000..b0958e9
--- /dev/null
@@ -0,0 +1,262 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/Chamber.java b/src/com/billkuker/rocketry/motorsim/Chamber.java
new file mode 100644 (file)
index 0000000..28c833e
--- /dev/null
@@ -0,0 +1,13 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/ConvergentDivergentNozzle.java b/src/com/billkuker/rocketry/motorsim/ConvergentDivergentNozzle.java
new file mode 100644 (file)
index 0000000..c9978ed
--- /dev/null
@@ -0,0 +1,82 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/CylindricalChamber.java b/src/com/billkuker/rocketry/motorsim/CylindricalChamber.java
new file mode 100644 (file)
index 0000000..cfac36d
--- /dev/null
@@ -0,0 +1,42 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/Fuel.java b/src/com/billkuker/rocketry/motorsim/Fuel.java
new file mode 100644 (file)
index 0000000..976ebd9
--- /dev/null
@@ -0,0 +1,43 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/Grain.java b/src/com/billkuker/rocketry/motorsim/Grain.java
new file mode 100644 (file)
index 0000000..8d36df3
--- /dev/null
@@ -0,0 +1,27 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/Motor.java b/src/com/billkuker/rocketry/motorsim/Motor.java
new file mode 100644 (file)
index 0000000..68a84ac
--- /dev/null
@@ -0,0 +1,40 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/Nozzle.java b/src/com/billkuker/rocketry/motorsim/Nozzle.java
new file mode 100644 (file)
index 0000000..e11a76c
--- /dev/null
@@ -0,0 +1,13 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/RocketScience.java b/src/com/billkuker/rocketry/motorsim/RocketScience.java
new file mode 100644 (file)
index 0000000..0300923
--- /dev/null
@@ -0,0 +1,16 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/fuel/KNSU.java b/src/com/billkuker/rocketry/motorsim/fuel/KNSU.java
new file mode 100644 (file)
index 0000000..bcd0cf0
--- /dev/null
@@ -0,0 +1,59 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/fuel/SaintRobertFuel.java b/src/com/billkuker/rocketry/motorsim/fuel/SaintRobertFuel.java
new file mode 100644 (file)
index 0000000..c6c3621
--- /dev/null
@@ -0,0 +1,58 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/grain/CoredCylindricalGrain.java b/src/com/billkuker/rocketry/motorsim/grain/CoredCylindricalGrain.java
new file mode 100644 (file)
index 0000000..8727657
--- /dev/null
@@ -0,0 +1,162 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/grain/ExtrudedGrain.java b/src/com/billkuker/rocketry/motorsim/grain/ExtrudedGrain.java
new file mode 100644 (file)
index 0000000..70ae575
--- /dev/null
@@ -0,0 +1,311 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/grain/GrainPanel.java b/src/com/billkuker/rocketry/motorsim/grain/GrainPanel.java
new file mode 100644 (file)
index 0000000..dd72821
--- /dev/null
@@ -0,0 +1,130 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/test/ConvergentDivergentNozzleTest.java b/src/com/billkuker/rocketry/motorsim/test/ConvergentDivergentNozzleTest.java
new file mode 100644 (file)
index 0000000..df97885
--- /dev/null
@@ -0,0 +1,69 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/test/CoredCylindricalGrainTest.java b/src/com/billkuker/rocketry/motorsim/test/CoredCylindricalGrainTest.java
new file mode 100644 (file)
index 0000000..a2fddab
--- /dev/null
@@ -0,0 +1,74 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/test/CylindricalChamberTest.java b/src/com/billkuker/rocketry/motorsim/test/CylindricalChamberTest.java
new file mode 100644 (file)
index 0000000..60600a7
--- /dev/null
@@ -0,0 +1,28 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/test/KNSUTest.java b/src/com/billkuker/rocketry/motorsim/test/KNSUTest.java
new file mode 100644 (file)
index 0000000..917dc77
--- /dev/null
@@ -0,0 +1,30 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/test/RocketTest.java b/src/com/billkuker/rocketry/motorsim/test/RocketTest.java
new file mode 100644 (file)
index 0000000..99f1cb8
--- /dev/null
@@ -0,0 +1,11 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/validation/Validating.java b/src/com/billkuker/rocketry/motorsim/validation/Validating.java
new file mode 100644 (file)
index 0000000..f84a742
--- /dev/null
@@ -0,0 +1,5 @@
+package com.billkuker.rocketry.motorsim.validation;\r
+\r
+public interface Validating {\r
+        void checkValidity() throws ValidationException;\r
+}\r
diff --git a/src/com/billkuker/rocketry/motorsim/validation/ValidationException.java b/src/com/billkuker/rocketry/motorsim/validation/ValidationException.java
new file mode 100644 (file)
index 0000000..88ae846
--- /dev/null
@@ -0,0 +1,12 @@
+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
diff --git a/src/com/billkuker/rocketry/motorsim/visual/Chart.java b/src/com/billkuker/rocketry/motorsim/visual/Chart.java
new file mode 100644 (file)
index 0000000..c4919fb
--- /dev/null
@@ -0,0 +1,185 @@
+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