def getMassFlux(self, massIn, dTime, regDist, dRegDist, position, density): diameter = self.props['diameter'].getValue() endPos = self.getEndPositions(regDist) # If a position above the top face is queried, the mass flow is just the input mass and the # diameter is the casting tube if position < endPos[0]: return massIn / geometry.circleArea(diameter) # If a position in the grain is queried, the mass flow is the input mass, from the top face, # and from the tube up to the point. The diameter is the core. if position <= endPos[1]: if self.props['inhibitedEnds'].getValue( ) == 'Top': # Top inhibited top = 0 countedCoreLength = position else: top = self.getFaceArea(regDist + dRegDist) * dRegDist * density countedCoreLength = position - (endPos[0] + dRegDist) # This block gets the mass of propellant the core burns in the step. core = ( (self.getPortArea(regDist + dRegDist) * countedCoreLength) - (self.getPortArea(regDist) * countedCoreLength)) core *= density massFlow = massIn + ((top + core) / dTime) return massFlow / self.getPortArea(regDist + dRegDist) # A position past the grain end was specified, so the mass flow includes the input mass flow # and all mass produced by the grain. Diameter is the casting tube. massFlow = massIn + (self.getVolumeSlice(regDist, dRegDist) * density / dTime) return massFlow / geometry.circleArea(diameter)
def getPortArea(self, regDist): faceArea = self.getFaceArea(regDist) uncored = geometry.circleArea(self.props['diameter'].getValue()) return uncored - faceArea
def runSimulation(self, callback=None): """Runs a simulation of the motor and returns a simRes instance with the results. Constraints are checked, including the number of grains, if the motor has a propellant set, and if the grains have geometry errors. If all of these tests are passed, the motor's operation is simulated by calculating Kn, using this value to get pressure, and using pressure to determine thrust and other statistics. The next timestep is then prepared by using the pressure to determine how the motor will regress in the given timestep at the current pressure. This process is repeated and regression tracked until all grains have burned out, when the results and any warnings are returned.""" burnoutWebThres = self.config.getProperty('burnoutWebThres') burnoutThrustThres = self.config.getProperty('burnoutThrustThres') dTime = self.config.getProperty('timestep') simRes = SimulationResult(self) # Check for geometry errors if len(self.grains) == 0: aText = 'Motor must have at least one propellant grain' simRes.addAlert( SimAlert(SimAlertLevel.ERROR, SimAlertType.CONSTRAINT, aText, 'Motor')) for gid, grain in enumerate(self.grains): if isinstance( grain, EndBurningGrain ) and gid != 0: # Endburners have to be at the foward end aText = 'End burning grains must be the forward-most grain in the motor' simRes.addAlert( SimAlert(SimAlertLevel.ERROR, SimAlertType.CONSTRAINT, aText, 'Grain ' + str(gid + 1))) for alert in grain.getGeometryErrors(): alert.location = 'Grain ' + str(gid + 1) simRes.addAlert(alert) for alert in self.nozzle.getGeometryErrors(): simRes.addAlert(alert) # Make sure the motor has a propellant set if self.propellant is None: alert = SimAlert(SimAlertLevel.ERROR, SimAlertType.CONSTRAINT, 'Motor must have a propellant set', 'Motor') simRes.addAlert(alert) else: for alert in self.propellant.getErrors(): simRes.addAlert(alert) # If any errors occurred, stop simulation and return an empty sim with errors if len(simRes.getAlertsByLevel(SimAlertLevel.ERROR)) > 0: print("Error in sim") print(simRes.getAlertsByLevel(SimAlertLevel.ERROR)[0].description) print("in", simRes.getAlertsByLevel(SimAlertLevel.ERROR)[0].location) return simRes # Pull the required numbers from the propellant density = self.propellant.getProperty('density') # Generate coremaps for perforated grains for grain in self.grains: grain.simulationSetup(self.config) # Setup initial values perGrainReg = [0 for grain in self.grains] # At t = 0, the motor has ignited simRes.channels['time'].addData(0) simRes.channels['kn'].addData(self.calcKN(perGrainReg, 0)) igniterPres = self.config.getProperty('igniterPressure') simRes.channels['pressure'].addData( self.calcIdealPressure(perGrainReg, 0, igniterPres, None)) simRes.channels['force'].addData(0) simRes.channels['mass'].addData([ grain.getVolumeAtRegression(0) * density for grain in self.grains ]) simRes.channels['massFlow'].addData([0 for grain in self.grains]) simRes.channels['massFlux'].addData([0 for grain in self.grains]) simRes.channels['regression'].addData([0 for grains in self.grains]) simRes.channels['web'].addData( [grain.getWebLeft(0) for grain in self.grains]) simRes.channels['exitPressure'].addData(0) simRes.channels['dThroat'].addData(0) # Check port/throat ratio and add a warning if it is large enough aftPort = self.grains[-1].getPortArea(0) if aftPort is not None: minAllowed = self.config.getProperty('minPortThroat') ratio = aftPort / geometry.circleArea( self.nozzle.props['throat'].getValue()) if ratio < minAllowed: desc = 'Initial port/throat ratio of ' + str(round( ratio, 3)) + ' was less than ' + str(minAllowed) simRes.addAlert( SimAlert(SimAlertLevel.WARNING, SimAlertType.CONSTRAINT, desc, 'N/A')) # Perform timesteps while simRes.shouldContinueSim(burnoutThrustThres): # Calculate regression massFlow = 0 perGrainMass = [0 for grain in self.grains] perGrainMassFlow = [0 for grain in self.grains] perGrainMassFlux = [0 for grain in self.grains] perGrainWeb = [0 for grain in self.grains] for gid, grain in enumerate(self.grains): if grain.getWebLeft(perGrainReg[gid]) > burnoutWebThres: # Calculate regression at the current pressure reg = dTime * self.propellant.getBurnRate( simRes.channels['pressure'].getLast()) # Find the mass flux through the grain based on the mass flow fed into from grains above it perGrainMassFlux[gid] = grain.getPeakMassFlux( massFlow, dTime, perGrainReg[gid], reg, density) # Find the mass of the grain after regression perGrainMass[gid] = grain.getVolumeAtRegression( perGrainReg[gid]) * density # Add the change in grain mass to the mass flow massFlow += (simRes.channels['mass'].getLast()[gid] - perGrainMass[gid]) / dTime # Apply the regression perGrainReg[gid] += reg perGrainWeb[gid] = grain.getWebLeft(perGrainReg[gid]) perGrainMassFlow[gid] = massFlow simRes.channels['regression'].addData(perGrainReg[:]) simRes.channels['web'].addData(perGrainWeb) simRes.channels['mass'].addData(perGrainMass) simRes.channels['massFlow'].addData(perGrainMassFlow) simRes.channels['massFlux'].addData(perGrainMassFlux) # Calculate KN dThroat = simRes.channels['dThroat'].getLast() simRes.channels['kn'].addData(self.calcKN(perGrainReg, dThroat)) # Calculate Pressure lastPressure = simRes.channels['pressure'].getLast() lastKn = simRes.channels['kn'].getLast() pressure = self.calcIdealPressure(perGrainReg, dThroat, lastPressure, lastKn) simRes.channels['pressure'].addData(pressure) # Calculate Exit Pressure _, _, gamma, _, _ = self.propellant.getCombustionProperties( pressure) exitPressure = self.nozzle.getExitPressure(gamma, pressure) simRes.channels['exitPressure'].addData(exitPressure) # Calculate force force = self.calcForce(simRes.channels['pressure'].getLast(), dThroat, exitPressure) simRes.channels['force'].addData(force) simRes.channels['time'].addData(simRes.channels['time'].getLast() + dTime) # Calculate any slag deposition or erosion of the throat if pressure == 0: slagRate = 0 else: slagRate = (1 / pressure) * self.nozzle.getProperty('slagCoeff') erosionRate = pressure * self.nozzle.getProperty('erosionCoeff') change = dTime * ((-2 * slagRate) + (2 * erosionRate)) simRes.channels['dThroat'].addData(dThroat + change) if callback is not None: # Uses the grain with the largest percentage of its web left progress = max([ g.getWebLeft(r) / g.getWebLeft(0) for g, r in zip(self.grains, perGrainReg) ]) if callback( 1 - progress ): # If the callback returns true, it is time to cancel return simRes simRes.success = True if simRes.getPeakMassFlux() > self.config.getProperty('maxMassFlux'): desc = 'Peak mass flux exceeded configured limit' alert = SimAlert(SimAlertLevel.WARNING, SimAlertType.CONSTRAINT, desc, 'Motor') simRes.addAlert(alert) if simRes.getMaxPressure() > self.config.getProperty('maxPressure'): desc = 'Max pressure exceeded configured limit' alert = SimAlert(SimAlertLevel.WARNING, SimAlertType.CONSTRAINT, desc, 'Motor') simRes.addAlert(alert) # Note that this only adds all errors found on the first datapoint where there were errors to avoid repeating # errors. It should be revisited if getPressureErrors ever returns multiple types of errors for pressure in simRes.channels['pressure'].getData(): if pressure > 0: err = self.propellant.getPressureErrors(pressure) if len(err) > 0: simRes.addAlert(err[0]) break return simRes
def getExitArea(self): """Return the area of the nozzle's exit.""" return geometry.circleArea(self.props['exit'].getValue())
def getThroatArea(self, dThroat=0): """Returns the area of the nozzle's throat. The optional parameter is added on to the nozzle throat diameter allow erosion or slag buildup during a burn.""" return geometry.circleArea(self.props['throat'].getValue() + dThroat)
def getPortRatio(self): """Returns the port/throat ratio of the motor, or None if it doesn't have a port.""" aftPort = self.motor.grains[-1].getPortArea(0) if aftPort is not None: return aftPort / geometry.circleArea(self.motor.nozzle.props['throat'].getValue()) return None