e101d8154b7edb12858277f4d9533ac783a2fad9
[sw/motorsim] / src / com / billkuker / rocketry / motorsim / Burn.java
1 package com.billkuker.rocketry.motorsim;\r
2 \r
3 \r
4 \r
5 import java.util.Date;\r
6 import java.util.HashSet;\r
7 import java.util.Set;\r
8 import java.util.SortedMap;\r
9 import java.util.TreeMap;\r
10 \r
11 import javax.measure.quantity.Area;\r
12 import javax.measure.quantity.Dimensionless;\r
13 import javax.measure.quantity.Duration;\r
14 import javax.measure.quantity.Force;\r
15 import javax.measure.quantity.Length;\r
16 import javax.measure.quantity.Mass;\r
17 import javax.measure.quantity.MassFlowRate;\r
18 import javax.measure.quantity.Pressure;\r
19 import javax.measure.quantity.Quantity;\r
20 import javax.measure.quantity.Temperature;\r
21 import javax.measure.quantity.Velocity;\r
22 import javax.measure.quantity.Volume;\r
23 import javax.measure.quantity.VolumetricDensity;\r
24 import javax.measure.unit.SI;\r
25 \r
26 import org.apache.log4j.Logger;\r
27 import org.jscience.physics.amount.Amount;\r
28 import org.jscience.physics.amount.Constants;\r
29 \r
30 import com.billkuker.rocketry.motorsim.Validating.ValidationException;\r
31 \r
32 public class Burn {\r
33         //Some constants to tune adaptive regression step\r
34         private static final double regStepIncreaseFactor = 1.01;\r
35         private static final double regStepDecreaseFactor = .5;\r
36         private static final Amount<Pressure> chamberPressureMaxDelta = Amount.valueOf(.5, SI.MEGA(SI.PASCAL));\r
37         \r
38         private static final Amount<Pressure> endPressure = Amount.valueOf(.1, RocketScience.PSI);\r
39         \r
40         private static Logger log = Logger.getLogger(Burn.class);\r
41         protected final Motor motor;\r
42         \r
43         private boolean burning = false;\r
44         private boolean done = false;\r
45         \r
46         public interface BurnProgressListener{\r
47                 public void setProgress(float p);\r
48                 public void burnComplete();\r
49         }\r
50         \r
51         private Set<BurnProgressListener> bpls = new HashSet<Burn.BurnProgressListener>();\r
52         \r
53         private static final Amount<Pressure> atmosphereicPressure = Amount.valueOf(101000, SI.PASCAL);\r
54         \r
55         public class Interval{\r
56                 public Amount<Duration> time;\r
57                 public Amount<Duration> dt;\r
58                 public Amount<Length> regression;\r
59                 public Amount<Pressure> chamberPressure;\r
60                 Amount<Mass> chamberProduct;\r
61                 public Amount<Force> thrust;\r
62 \r
63                 public String toString(){\r
64                         return time + " " + dt + " " + regression + " " + chamberPressure + " " + chamberProduct;\r
65                 }\r
66         }\r
67         \r
68         protected SortedMap<Amount<Duration>,Interval> data = new TreeMap<Amount<Duration>, Interval>();\r
69         \r
70         public SortedMap<Amount<Duration>,Interval> getData(){\r
71                 if ( !done )\r
72                         throw new IllegalStateException("Burn not complete!");\r
73                 return data;\r
74         }\r
75         \r
76         public Motor getMotor(){\r
77                 return motor;\r
78         }\r
79 \r
80         public Amount<Duration> burnTime(){\r
81                 return getData().lastKey();\r
82         }\r
83         \r
84         public Burn(Motor m){\r
85                 try {\r
86                         m.validate();\r
87                 } catch (ValidationException e) {\r
88                         throw new IllegalArgumentException("Invalid Motor: " + e.getMessage());\r
89                 }\r
90                 motor = m;\r
91         }\r
92         \r
93         public void addBurnProgressListener( BurnProgressListener bpl ){\r
94                 bpls.add(bpl);\r
95                 if ( done )\r
96                         bpl.burnComplete();\r
97         }\r
98         \r
99         public void burn(){\r
100                 synchronized(this){\r
101                         if ( burning )\r
102                                 throw new IllegalStateException("Already burning!");\r
103                         burning = true;\r
104                 }\r
105                 log.info("Starting burn...");\r
106                 for (BurnProgressListener bpl : bpls ){\r
107                         bpl.setProgress(0);\r
108                 }\r
109                 \r
110                 int endPressureSteps = 0;\r
111                 long start = new Date().getTime();\r
112                 \r
113                 Amount<Length> regStep = Amount.valueOf(0.01, SI.MILLIMETER);\r
114 \r
115                 Interval initial = new Interval();\r
116                 initial.time = Amount.valueOf(0, SI.SECOND);\r
117                 initial.dt = Amount.valueOf(0, SI.SECOND);\r
118                 initial.regression = Amount.valueOf(0, SI.MILLIMETER);\r
119                 initial.chamberPressure = atmosphereicPressure;\r
120                 initial.chamberProduct = Amount.valueOf(0, SI.KILOGRAM);\r
121                 initial.thrust = Amount.valueOf(0, SI.NEWTON);\r
122                 \r
123                 data.put(Amount.valueOf(0, SI.SECOND), initial);\r
124                 \r
125                 step:\r
126                 for ( int i = 0; i < 5000; i++ ) {\r
127                         assert(positive(regStep));\r
128                         regStep = regStep.times(regStepIncreaseFactor);\r
129                         \r
130                         Interval prev = data.get(data.lastKey());\r
131                         log.debug(prev);\r
132                         log.debug("Step " + i + " ==============================");\r
133                         Interval next = new Interval();\r
134                         \r
135                         Amount<Velocity> burnRate = motor.getFuel().burnRate(prev.chamberPressure);\r
136                         assert(positive(burnRate));\r
137                         \r
138                         log.debug("Burn Rate: " + burnRate);\r
139                         \r
140                         Amount<Duration> dt = regStep.divide(burnRate).to(Duration.UNIT);\r
141                         assert(positive(dt));\r
142                         next.dt = dt;\r
143                         \r
144 \r
145                         \r
146                         log.debug("Dt: " + dt);\r
147                         \r
148                         next.regression = prev.regression.plus(regStep);\r
149                         assert(positive(next.regression));\r
150                         \r
151                         log.debug("Regression: " + next.regression);\r
152                         \r
153                         //Update BurnProgressListeners\r
154                         Amount<Dimensionless> a = next.regression.divide(motor.getGrain().webThickness()).to(Dimensionless.UNIT);\r
155                         for (BurnProgressListener bpl : bpls ){\r
156                                 bpl.setProgress((float)a.doubleValue(Dimensionless.UNIT));\r
157                         }\r
158 \r
159                         \r
160                         next.time = prev.time.plus(dt);\r
161                         \r
162                         //log.debug("Vold: " + motor.getGrain().volume(prev.regression).to(SI.MILLIMETER.pow(3)));\r
163                         \r
164                         //log.debug("Vnew: " + motor.getGrain().volume(next.regression).to(SI.MILLIMETER.pow(3)));\r
165                         \r
166                         //TODO Amount<Volume> volumeBurnt = motor.getGrain().volume(prev.regression).minus(motor.getGrain().volume(next.regression));\r
167                         Amount<Volume> volumeBurnt = motor.getGrain().surfaceArea(prev.regression).times(regStep).to(Volume.UNIT);\r
168                         assert(positive(volumeBurnt));\r
169                         //log.info("Volume Burnt: " + volumeBurnt.to(SI.MILLIMETER.pow(3)));\r
170                         \r
171                         Amount<MassFlowRate> mGenRate = volumeBurnt.times(motor.getFuel().getIdealDensity().times(motor.getFuel().getDensityRatio())).divide(dt).to(MassFlowRate.UNIT);\r
172                         assert(positive(mGenRate));\r
173                         \r
174                         //log.debug("Mass Gen Rate: " + mGenRate);\r
175                         \r
176                         //Calculate specific gas constant\r
177                         Amount<?> specificGasConstant = Constants.R.divide(motor.getFuel().getCombustionProduct().getEffectiveMolarWeight());\r
178                         //This unit conversion helps JScience to convert nozzle flow rate to\r
179                         //kg/s a little later on I verified the conversion by hand and\r
180                         //JScience checks it too.\r
181                         specificGasConstant = convertSpecificGasConstantUnits(specificGasConstant);\r
182                         \r
183                         //Calculate chamber temperature\r
184                         Amount<Temperature> chamberTemp = motor.getFuel().getCombustionProduct().getIdealCombustionTemperature().times(motor.getFuel().getCombustionEfficiency());\r
185                         \r
186                         Amount<MassFlowRate> mNozzle;\r
187                         {\r
188                                 Amount<Pressure> pDiff = prev.chamberPressure.minus(atmosphereicPressure);\r
189                                 //log.debug("Pdiff: " + pDiff);\r
190                                 Amount<Area> aStar = motor.getNozzle().throatArea();\r
191                                 double k = motor.getFuel().getCombustionProduct().getRatioOfSpecificHeats();\r
192                                 double kSide = Math.sqrt(k) * Math.pow((2/(k+1)) , (((k+1)/2)/(k-1)));\r
193                                 Amount<?> sqrtPart = specificGasConstant.times(chamberTemp).sqrt();\r
194                                 mNozzle = pDiff.times(aStar).times(kSide).divide(sqrtPart).to(MassFlowRate.UNIT);\r
195                                 //log.debug("Mass Exit Rate: " + mNozzle.to(MassFlowRate.UNIT));                \r
196                         }\r
197                         assert(positive(mNozzle));\r
198                         \r
199                         Amount<MassFlowRate> massStorageRate = mGenRate.minus(mNozzle);\r
200                         \r
201                         //log.debug("Mass Storage Rate: " + massStorageRate);\r
202 \r
203                         next.chamberProduct = prev.chamberProduct.plus(massStorageRate.times(dt));\r
204                         \r
205                         //Product can not go negative!\r
206                         if ( !positive(next.chamberProduct) ){\r
207                                 log.warn("ChamberProduct Negative on step " + i + "!, Adjusting regstep down and repeating step!");\r
208                                 regStep = regStep.times(regStepDecreaseFactor);\r
209                                 continue step;\r
210                         }\r
211                         assert(positive(next.chamberProduct));\r
212                         if ( next.chamberProduct.isLessThan(Amount.valueOf(0, SI.KILOGRAM)) )\r
213                                 next.chamberProduct = Amount.valueOf(0, SI.KILOGRAM);\r
214                         \r
215                         //log.debug("Chamber Product: " + next.chamberProduct);\r
216                         \r
217                         Amount<VolumetricDensity> combustionProductDensity = next.chamberProduct.divide(motor.getChamber().chamberVolume().minus(motor.getGrain().volume(next.regression))).to(VolumetricDensity.UNIT);\r
218                         \r
219                         log.debug("Product Density: " + combustionProductDensity);\r
220                         \r
221                         next.chamberPressure = combustionProductDensity.times(specificGasConstant).times(chamberTemp).plus(atmosphereicPressure).to(Pressure.UNIT);\r
222                         assert(positive(next.chamberPressure));\r
223                         \r
224                         next.chamberPressure = Amount.valueOf(\r
225                                         next.chamberPressure.doubleValue(SI.PASCAL),\r
226                                         SI.PASCAL);\r
227                         \r
228                         Amount<Pressure> dp = next.chamberPressure.minus(prev.chamberPressure);\r
229                         if ( dp.abs().isGreaterThan(chamberPressureMaxDelta)){\r
230                                 log.warn("DP " + dp + " too big!, Adjusting regstep down and repeating step!");\r
231                                 regStep = regStep.times(regStepDecreaseFactor);\r
232                                 continue step;\r
233                         }\r
234                         \r
235                         next.thrust = motor.getNozzle().thrust(next.chamberPressure, atmosphereicPressure, atmosphereicPressure, motor.getFuel().getCombustionProduct().getRatioOfSpecificHeats2Phase());\r
236                         assert(positive(next.thrust));\r
237                         \r
238                         if ( i > 100 && next.chamberPressure.minus(atmosphereicPressure).abs().isLessThan(endPressure)){\r
239                                 log.info("Pressure at ~Patm on step " + i);\r
240                                 endPressureSteps++;\r
241                                 if ( endPressureSteps > 5 )\r
242                                         break;\r
243                         }\r
244                         \r
245                         data.put(data.lastKey().plus(dt), next);\r
246                 }\r
247 \r
248                 long time = new Date().getTime() - start;\r
249                 log.info("Burn took " + time + " millis.");\r
250                 done = true;\r
251                 for (BurnProgressListener bpl : bpls ){\r
252                         bpl.burnComplete();\r
253                 }\r
254         }\r
255         \r
256         @SuppressWarnings("unchecked")\r
257         /*\r
258          * This converts the units of this constant to something JScience is able\r
259          * to work from. This conversion is unchecked at compile time, but\r
260          * JScience keeps me honest at runtime.\r
261          */\r
262         private Amount convertSpecificGasConstantUnits(Amount a){\r
263                 return a.to(\r
264                                 SI.METER.pow(2).divide(SI.SECOND.pow(2).times(SI.KELVIN)));\r
265         }\r
266         \r
267         public Amount<Pressure> pressure(Amount<Duration> time){\r
268                 return getData().get(time).chamberPressure;\r
269         }\r
270         \r
271         public Amount<Force> thrust(Amount<Duration> time){\r
272                 return getData().get(time).thrust;\r
273         }\r
274         \r
275         public Amount<Dimensionless> kn(Amount<Length> regression){\r
276                 return motor.getGrain().surfaceArea(regression).divide(motor.getNozzle().throatArea()).to(Dimensionless.UNIT);\r
277         }\r
278         \r
279         \r
280         private <Q extends Quantity> boolean positive(Amount<Q> a){\r
281                 return ( a.isGreaterThan(a.minus(a)) || a.equals(a.minus(a)));\r
282         }\r
283 \r
284 }\r