diff --git a/app.py b/app.py index 833c0d0..f22caf0 100644 --- a/app.py +++ b/app.py @@ -50,10 +50,10 @@ def __init__(self, args): motor = self.fileManager.getCurrentMotor() simulationResult = motor.runSimulation() for alert in simulationResult.alerts: - print('{} ({}, {}): {}'.format(motorlib.simResult.alertLevelNames[alert.level], - motorlib.simResult.alertTypeNames[alert.type], - alert.location, - alert.description)) + print('{} ({}, {}): {}'.format(alert.level, + alert.type, + alert.location, + alert.description)) print() if '-o' in args: with open(args[args.index('-o') + 1], 'w') as outputFile: diff --git a/motorlib/enums/__init__.py b/motorlib/enums/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/motorlib/enums/inhibitedEnds.py b/motorlib/enums/inhibitedEnds.py new file mode 100644 index 0000000..60803d7 --- /dev/null +++ b/motorlib/enums/inhibitedEnds.py @@ -0,0 +1,10 @@ +from enum import Enum + + +# Python 3.11 supports `StrEnum` that would make this a bit more concise to write +# https://docs.python.org/3/library/enum.html#enum.StrEnum +class InhibitedEnds(str, Enum): + NEITHER = 'Neither' + TOP = 'Top' + BOTTOM = 'Bottom' + BOTH = 'Both' diff --git a/motorlib/enums/multiValueChannels.py b/motorlib/enums/multiValueChannels.py new file mode 100644 index 0000000..b98d42f --- /dev/null +++ b/motorlib/enums/multiValueChannels.py @@ -0,0 +1,11 @@ +from enum import Enum + + +# Python 3.11 supports `StrEnum` that would make this a bit more concise to write +# https://docs.python.org/3/library/enum.html#enum.StrEnum +class MultiValueChannels(str, Enum): + MASS = 'mass' + MASS_FLOW = 'massFlow' + MASS_FLUX = 'massFlux' + REGRESSION = 'regression' + WEB = 'web' diff --git a/motorlib/enums/simAlertLevel.py b/motorlib/enums/simAlertLevel.py new file mode 100644 index 0000000..7235c17 --- /dev/null +++ b/motorlib/enums/simAlertLevel.py @@ -0,0 +1,10 @@ +from enum import Enum + + +# Python 3.11 supports `StrEnum` that would make this a bit more concise to write +# https://docs.python.org/3/library/enum.html#enum.StrEnum +class SimAlertLevel(str, Enum): + """Levels of severity for sim alerts""" + ERROR = 'Error' + WARNING = 'Warning' + MESSAGE = 'Message' diff --git a/motorlib/enums/simAlertType.py b/motorlib/enums/simAlertType.py new file mode 100644 index 0000000..8477126 --- /dev/null +++ b/motorlib/enums/simAlertType.py @@ -0,0 +1,9 @@ +from enum import Enum + +# Python 3.11 supports `StrEnum` that would make this a bit more concise to write +# https://docs.python.org/3/library/enum.html#enum.StrEnum +class SimAlertType(str, Enum): + """Types of sim alerts""" + GEOMETRY = 'Geometry' + CONSTRAINT = 'Constraint' + VALUE = 'Value' diff --git a/motorlib/enums/singleValueChannels.py b/motorlib/enums/singleValueChannels.py new file mode 100644 index 0000000..b47c99f --- /dev/null +++ b/motorlib/enums/singleValueChannels.py @@ -0,0 +1,13 @@ +from enum import Enum + + +# Python 3.11 supports `StrEnum` that would make this a bit more concise to write +# https://docs.python.org/3/library/enum.html#enum.StrEnum +class SingleValueChannels(str, Enum): + TIME = 'time' + KN = 'kn' + PRESSURE = 'pressure' + FORCE = 'force' + VOLUME_LOADING = 'volumeLoading' + EXIT_PRESSURE = 'exitPressure' + D_THROAT = 'dThroat' diff --git a/motorlib/enums/unit.py b/motorlib/enums/unit.py new file mode 100644 index 0000000..79dc8ae --- /dev/null +++ b/motorlib/enums/unit.py @@ -0,0 +1,83 @@ +from enum import Enum + + +# Python 3.11 supports `StrEnum` that would make this a bit more concise to write +# https://docs.python.org/3/library/enum.html#enum.StrEnum +class Unit(str, Enum): + # Angle + DEGREES = 'deg' + + # Burn Rate Coefficient + METER_PER_SECOND_PASCAL_TO_THE_POWER_OF_N = 'm/(s*Pa^n)' + INCH_PER_SECOND_POUND_PER_SQUARE_INCH_TO_THE_POWER_OF_N = 'in/(s*psi^n)' + + # Density + KILOGRAM_PER_CUBIC_METER = 'kg/m^3' + POUND_PER_CUBIC_INCH = 'lb/in^3' + GRAM_PER_CUBIC_CENTIMETER = 'g/cm^3' + + # Force + NEWTON = 'N' + POUND_FORCE = 'lbf' + + # Impulse + NEWTON_SECOND = 'Ns' + POUND_FORCE_SECOND = 'lbfs' + + # Length + METER = 'm' + CENTIMETER = 'cm' + MILLIMETER = 'mm' + INCH = 'in' + FOOT = 'ft' + + # Mass Flow + KILOGRAM_PER_SECOND = 'kg/s' + POUND_PER_SECOND = 'lb/s' + GRAM_PER_SECOND = 'g/s' + + # Mass Flux + KILOGRAM_PER_SQUARE_METER_PER_SECOND = 'kg/(m^2*s)' + POUND_PER_SQUARE_INCH_PER_SECOND = 'lb/(in^2*s)' + + # Mass + KILOGRAM = 'kg' + GRAM = 'g' + POUND = 'lb' + OUNCE = 'oz' + GRAM_PER_MOLE = 'g/mol' + + # Nozzle Erosion Coefficient + METER_PER_SECOND_PASCAL = 'm/(s*Pa)' + METER_PER_SECOND_MEGAPASCAL = 'm/(s*MPa)' + THOUSANDTH_INCH_PER_SECOND_POUND_PER_SQUARE_INCH = 'thou/(s*psi)' + + # Nozzle Slag Coefficient + METER_PASCAL_PER_SECOND = '(m*Pa)/s' + METER_MEGAPASCAL_PER_SECOND = '(m*MPa)/s' + INCH_POUND_PER_SQUARE_INCH_PER_SECOND = '(in*psi)/s' + + # Pressure + PASCAL = 'Pa' + MEGAPASCAL = 'MPa' + POUND_PER_SQUARE_INCH = 'psi' + + # Temperature + KELVIN = 'K' + + # Time + SECOND = 's' + + # Velocity + METER_PER_SECOND = 'm/s' + CENTIMETER_PER_SECOND = 'cm/s' + MILLIMETER_PER_SECOND = 'mm/s' + FOOT_PER_SECOND = 'ft/s' + INCH_PER_SECOND = 'in/s' + + # Volume + CUBIC_METER = 'm^3' + CUBIC_CENTIMETER = 'cm^3' + CUBIC_MILLIMETER = 'mm^3' + CUBIC_INCH = 'in^3' + CUBIC_FOOT = 'ft^3' \ No newline at end of file diff --git a/motorlib/grain.py b/motorlib/grain.py index 24d2e1f..50ac3ee 100644 --- a/motorlib/grain.py +++ b/motorlib/grain.py @@ -10,7 +10,11 @@ from scipy import interpolate from . import geometry -from .simResult import SimAlert, SimAlertLevel, SimAlertType +from .enums.inhibitedEnds import InhibitedEnds +from .enums.simAlertLevel import SimAlertLevel +from .enums.simAlertType import SimAlertType +from .enums.unit import Unit +from .simResult import SimAlert from .properties import FloatProperty, EnumProperty, PropertyCollection class Grain(PropertyCollection): @@ -19,8 +23,8 @@ class Grain(PropertyCollection): geomName = None def __init__(self): super().__init__() - self.props['diameter'] = FloatProperty('Diameter', 'm', 0, 1) - self.props['length'] = FloatProperty('Length', 'm', 0, 3) + self.props['diameter'] = FloatProperty('Diameter', Unit.METER, 0, 1) + self.props['length'] = FloatProperty('Length', Unit.METER, 0, 3) def getVolumeSlice(self, regDist, dRegDist): """Returns the amount of propellant volume consumed as the grain regresses from a distance of 'regDist' to @@ -67,9 +71,9 @@ def getRegressedLength(self, regDist): endPos = self.getEndPositions(regDist) return endPos[1] - endPos[0] - def getDetailsString(self, lengthUnit='m'): + def getDetailsString(self, Unit=Unit.METER): """Returns a short string describing the grain, formatted using the units that is passed in""" - return 'Length: {}'.format(self.props['length'].dispFormat(lengthUnit)) + return 'Length: {}'.format(self.props['length'].dispFormat(Unit)) @abstractmethod def simulationSetup(self, config): @@ -103,17 +107,20 @@ class PerforatedGrain(Grain): geomName = 'perfGrain' def __init__(self): super().__init__() - self.props['inhibitedEnds'] = EnumProperty('Inhibited ends', ['Neither', 'Top', 'Bottom', 'Both']) + self.props['inhibitedEnds'] = EnumProperty('Inhibited ends', [InhibitedEnds.NEITHER, + InhibitedEnds.TOP, + InhibitedEnds.BOTTOM, + InhibitedEnds.BOTH]) self.wallWeb = 0 # Max distance from the core to the wall def getEndPositions(self, regDist): - if self.props['inhibitedEnds'].getValue() == 'Neither': # Neither + if self.props['inhibitedEnds'].getValue() == InhibitedEnds.NEITHER: return (regDist, self.props['length'].getValue() - regDist) - if self.props['inhibitedEnds'].getValue() == 'Top': # Top + if self.props['inhibitedEnds'].getValue() == InhibitedEnds.TOP: return (0, self.props['length'].getValue() - regDist) - if self.props['inhibitedEnds'].getValue() == 'Bottom': # Bottom + if self.props['inhibitedEnds'].getValue() == InhibitedEnds.BOTTOM: return (regDist, self.props['length'].getValue()) - if self.props['inhibitedEnds'].getValue() == 'Both': + if self.props['inhibitedEnds'].getValue() == InhibitedEnds.BOTH: return (0, self.props['length'].getValue()) # The enum should prevent this from even being raised, but to cover the case where it somehow gets set wrong raise ValueError('Invalid number of faces inhibited') @@ -135,7 +142,7 @@ def getCoreSurfaceArea(self, regDist): def getWebLeft(self, regDist): wallLeft = self.wallWeb - regDist - if self.props['inhibitedEnds'].getValue() == 'Both': + if self.props['inhibitedEnds'].getValue() == InhibitedEnds.BOTH: return wallLeft lengthLeft = self.getRegressedLength(regDist) return min(lengthLeft, wallLeft) @@ -145,9 +152,9 @@ def getSurfaceAreaAtRegression(self, regDist): coreArea = self.getCoreSurfaceArea(regDist) exposedFaces = 2 - if self.props['inhibitedEnds'].getValue() == 'Top' or self.props['inhibitedEnds'].getValue() == 'Bottom': + if self.props['inhibitedEnds'].getValue() == InhibitedEnds.TOP or self.props['inhibitedEnds'].getValue() == InhibitedEnds.BOTTOM: exposedFaces = 1 - if self.props['inhibitedEnds'].getValue() == 'Both': + if self.props['inhibitedEnds'].getValue() == InhibitedEnds.BOTH: exposedFaces = 0 return coreArea + (exposedFaces * faceArea) @@ -172,7 +179,7 @@ def getMassFlux(self, massIn, dTime, regDist, dRegDist, position, density): # 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() in ('Top', 'Both'): + if self.props['inhibitedEnds'].getValue() in (InhibitedEnds.TOP, InhibitedEnds.BOTH): top = 0 countedCoreLength = position else: diff --git a/motorlib/grains/bates.py b/motorlib/grains/bates.py index 29ab093..8821cb0 100644 --- a/motorlib/grains/bates.py +++ b/motorlib/grains/bates.py @@ -4,9 +4,12 @@ import skfmm from skimage import measure +from ..enums.simAlertLevel import SimAlertLevel +from ..enums.simAlertType import SimAlertType +from ..enums.unit import Unit from ..grain import PerforatedGrain from .. import geometry -from ..simResult import SimAlert, SimAlertLevel, SimAlertType +from ..simResult import SimAlert from ..properties import FloatProperty class BatesGrain(PerforatedGrain): @@ -15,7 +18,7 @@ class BatesGrain(PerforatedGrain): geomName = "BATES" def __init__(self): super().__init__() - self.props['coreDiameter'] = FloatProperty('Core Diameter', 'm', 0, 1) + self.props['coreDiameter'] = FloatProperty('Core Diameter', Unit.METER, 0, 1) def simulationSetup(self, config): self.wallWeb = (self.props['diameter'].getValue() - self.props['coreDiameter'].getValue()) / 2 @@ -28,9 +31,9 @@ def getFaceArea(self, regDist): inner = geometry.circleArea(self.props['coreDiameter'].getValue() + (2 * regDist)) return outer - inner - def getDetailsString(self, lengthUnit='m'): - return 'Length: {}, Core: {}'.format(self.props['length'].dispFormat(lengthUnit), - self.props['coreDiameter'].dispFormat(lengthUnit)) + def getDetailsString(self, Unit=Unit.METER): + return 'Length: {}, Core: {}'.format(self.props['length'].dispFormat(Unit), + self.props['coreDiameter'].dispFormat(Unit)) def getGeometryErrors(self): errors = super().getGeometryErrors() diff --git a/motorlib/grains/cGrain.py b/motorlib/grains/cGrain.py index 6010cda..fca3e4e 100644 --- a/motorlib/grains/cGrain.py +++ b/motorlib/grains/cGrain.py @@ -2,9 +2,12 @@ import numpy as np +from ..enums.simAlertLevel import SimAlertLevel +from ..enums.simAlertType import SimAlertType +from ..enums.unit import Unit from ..grain import FmmGrain from ..properties import FloatProperty -from ..simResult import SimAlert, SimAlertLevel, SimAlertType +from ..simResult import SimAlert class CGrain(FmmGrain): """Defines a C grain, which is a cylindrical grain with a single slot taken out. The slot is a rectangular section @@ -13,8 +16,8 @@ class CGrain(FmmGrain): geomName = 'C Grain' def __init__(self): super().__init__() - self.props['slotWidth'] = FloatProperty('Slot width', 'm', 0, 1) - self.props['slotOffset'] = FloatProperty('Slot offset', 'm', -1, 1) + self.props['slotWidth'] = FloatProperty('Slot width', Unit.METER, 0, 1) + self.props['slotOffset'] = FloatProperty('Slot offset', Unit.METER, -1, 1) self.props['slotOffset'].setValue(0) @@ -24,8 +27,8 @@ def generateCoreMap(self): self.coreMap[np.logical_and(np.abs(self.mapY) < slotWidth / 2, self.mapX > slotOffset)] = 0 - def getDetailsString(self, lengthUnit='m'): - return 'Length: {}'.format(self.props['length'].dispFormat(lengthUnit)) + def getDetailsString(self, Unit=Unit.METER): + return 'Length: {}'.format(self.props['length'].dispFormat(Unit)) def getGeometryErrors(self): errors = super().getGeometryErrors() diff --git a/motorlib/grains/conical.py b/motorlib/grains/conical.py index 886fee1..327bfcf 100644 --- a/motorlib/grains/conical.py +++ b/motorlib/grains/conical.py @@ -5,9 +5,13 @@ from skimage import measure from math import atan, cos, sin +from ..enums.inhibitedEnds import InhibitedEnds +from ..enums.simAlertLevel import SimAlertLevel +from ..enums.simAlertType import SimAlertType +from ..enums.unit import Unit from ..grain import Grain from .. import geometry -from ..simResult import SimAlert, SimAlertLevel, SimAlertType +from ..simResult import SimAlert from ..properties import FloatProperty, EnumProperty class ConicalGrain(Grain): @@ -15,9 +19,9 @@ class ConicalGrain(Grain): geomName = "Conical" def __init__(self): super().__init__() - self.props['forwardCoreDiameter'] = FloatProperty('Forward Core Diameter', 'm', 0, 1) - self.props['aftCoreDiameter'] = FloatProperty('Aft Core Diameter', 'm', 0, 1) - self.props['inhibitedEnds'] = EnumProperty('Inhibited ends', ['Both']) + self.props['forwardCoreDiameter'] = FloatProperty('Forward Core Diameter', Unit.METER, 0, 1) + self.props['aftCoreDiameter'] = FloatProperty('Aft Core Diameter', Unit.METER, 0, 1) + self.props['inhibitedEnds'] = EnumProperty('Inhibited ends', [InhibitedEnds.BOTH]) def isCoreInverted(self): """A simple helper that returns 'true' if the core's foward diameter is larger than its aft diameter""" @@ -33,9 +37,9 @@ def getFrustumInfo(self, regDist): exposedFaces = 0 inhibitedEnds = self.props['inhibitedEnds'].getValue() - if inhibitedEnds == 'Neither': + if inhibitedEnds == InhibitedEnds.NEITHER: exposedFaces = 2 - elif inhibitedEnds in ['Top', 'Bottom']: + elif inhibitedEnds in [InhibitedEnds.TOP, InhibitedEnds.BOTTOM]: exposedFaces = 1 # These calculations are easiest if we work in terms of the core's "large end" and "small end" @@ -86,9 +90,9 @@ def getSurfaceAreaAtRegression(self, regDist): surfaceArea = geometry.frustumLateralSurfaceArea(aftDiameter, forwardDiameter, length) fullFaceArea = geometry.circleArea(self.props['diameter'].getValue()) - if self.props['inhibitedEnds'].getValue() in ['Neither', 'Bottom']: + if self.props['inhibitedEnds'].getValue() in [InhibitedEnds.NEITHER, InhibitedEnds.BOTTOM]: surfaceArea += fullFaceArea - geometry.circleArea(forwardDiameter) - if self.props['inhibitedEnds'].getValue() in ['Neither', 'Top']: + if self.props['inhibitedEnds'].getValue() in [InhibitedEnds.NEITHER, InhibitedEnds.TOP]: surfaceArea += fullFaceArea - geometry.circleArea(aftDiameter) return surfaceArea @@ -159,7 +163,7 @@ def getEndPositions(self, regDist): inhibitedEnds = self.props['inhibitedEnds'].getValue() if self.isCoreInverted(): - if inhibitedEnds == 'Bottom' or inhibitedEnds == 'Both': + if inhibitedEnds == InhibitedEnds.BOTTOM or inhibitedEnds == InhibitedEnds.BOTH: # Because all of the change in length is due to the forward end moving, the forward end's position is # just the amount the the grain has regressed by, The aft end stays where it started return (originalLength - currentLength, originalLength) @@ -169,7 +173,7 @@ def getEndPositions(self, regDist): # total change in grain length to get the forward end position return (originalLength - currentLength - regDist, originalLength - regDist) - if inhibitedEnds == 'Top' or inhibitedEnds == 'Both': + if inhibitedEnds == InhibitedEnds.TOP or inhibitedEnds == InhibitedEnds.BOTH: # All of the change in grain length is due to the aft face moving, so the forward face stays at 0 and the # aft face is at the regressed grain length return (0, currentLength) @@ -183,9 +187,9 @@ def getPortArea(self, regDist): return geometry.circleArea(aftCoreDiameter) - def getDetailsString(self, lengthUnit='m'): + def getDetailsString(self, Unit=Unit.METER): """Returns a short string describing the grain, formatted using the units that is passed in""" - return 'Length: {}'.format(self.props['length'].dispFormat(lengthUnit)) + return 'Length: {}'.format(self.props['length'].dispFormat(Unit)) def simulationSetup(self, config): """Do anything needed to prepare this grain for simulation""" diff --git a/motorlib/grains/custom.py b/motorlib/grains/custom.py index d0640bd..404366a 100644 --- a/motorlib/grains/custom.py +++ b/motorlib/grains/custom.py @@ -2,9 +2,12 @@ import skimage.draw as draw +from ..enums.simAlertLevel import SimAlertLevel +from ..enums.simAlertType import SimAlertType +from ..enums.unit import Unit from ..grain import FmmGrain from ..properties import PolygonProperty, EnumProperty -from ..simResult import SimAlert, SimAlertLevel, SimAlertType +from ..simResult import SimAlert from ..units import getAllConversions, convert class CustomGrain(FmmGrain): @@ -15,13 +18,13 @@ class CustomGrain(FmmGrain): def __init__(self): super().__init__() self.props['points'] = PolygonProperty('Core geometry') - self.props['dxfUnit'] = EnumProperty('DXF Unit', getAllConversions('m')) + self.props['dxfUnit'] = EnumProperty('DXF Unit', getAllConversions(Unit.METER)) def generateCoreMap(self): inUnit = self.props['dxfUnit'].getValue() for polygon in self.props['points'].getValue(): - row = [(self.mapDim/2) + (-self.normalize(convert(p[1], inUnit, 'm')) * (self.mapDim/2)) for p in polygon] - col = [(self.mapDim/2) + (self.normalize(convert(p[0], inUnit, 'm')) * (self.mapDim/2)) for p in polygon] + row = [(self.mapDim/2) + (-self.normalize(convert(p[1], inUnit, Unit.METER)) * (self.mapDim/2)) for p in polygon] + col = [(self.mapDim/2) + (self.normalize(convert(p[0], inUnit, Unit.METER)) * (self.mapDim/2)) for p in polygon] imageRow, imageCol = draw.polygon(row, col, self.coreMap.shape) self.coreMap[imageRow, imageCol] = 0 diff --git a/motorlib/grains/dGrain.py b/motorlib/grains/dGrain.py index 53da4e0..4fbebcf 100644 --- a/motorlib/grains/dGrain.py +++ b/motorlib/grains/dGrain.py @@ -1,8 +1,10 @@ """D Grain submodule""" - +from ..enums.simAlertLevel import SimAlertLevel +from ..enums.simAlertType import SimAlertType +from ..enums.unit import Unit from ..grain import FmmGrain from ..properties import FloatProperty -from ..simResult import SimAlert, SimAlertLevel, SimAlertType +from ..simResult import SimAlert class DGrain(FmmGrain): """Defines a D grain, which is a grain that has no propellant past a chord that is a user-specified distance from @@ -10,7 +12,7 @@ class DGrain(FmmGrain): geomName = 'D Grain' def __init__(self): super().__init__() - self.props['slotOffset'] = FloatProperty('Slot offset', 'm', -1, 1) + self.props['slotOffset'] = FloatProperty('Slot offset', Unit.METER, -1, 1) self.props['slotOffset'].setValue(0) @@ -19,9 +21,9 @@ def generateCoreMap(self): self.coreMap[self.mapX > slotOffset] = 0 - def getDetailsString(self, lengthUnit='m'): - return 'Length: {}, Slot offset: {}'.format(self.props['length'].dispFormat(lengthUnit), - self.props['slotOffset'].dispFormat(lengthUnit)) + def getDetailsString(self, Unit=Unit.METER): + return 'Length: {}, Slot offset: {}'.format(self.props['length'].dispFormat(Unit), + self.props['slotOffset'].dispFormat(Unit)) def getGeometryErrors(self): errors = super().getGeometryErrors() diff --git a/motorlib/grains/finocyl.py b/motorlib/grains/finocyl.py index f82c53d..66f9b23 100644 --- a/motorlib/grains/finocyl.py +++ b/motorlib/grains/finocyl.py @@ -2,9 +2,12 @@ import numpy as np +from ..enums.simAlertLevel import SimAlertLevel +from ..enums.simAlertType import SimAlertType +from ..enums.unit import Unit from ..grain import FmmGrain from ..properties import FloatProperty, IntProperty -from ..simResult import SimAlert, SimAlertLevel, SimAlertType +from ..simResult import SimAlert class Finocyl(FmmGrain): """A finocyl (fins on cylinder) grain has a circular core with a number of rectangular extensions that start at the @@ -13,9 +16,9 @@ class Finocyl(FmmGrain): def __init__(self): super().__init__() self.props['numFins'] = IntProperty('Number of fins', '', 0, 64) - self.props['finWidth'] = FloatProperty('Fin width', 'm', 0, 1) - self.props['finLength'] = FloatProperty('Fin length', 'm', 0, 1) - self.props['coreDiameter'] = FloatProperty('Core diameter', 'm', 0, 1) + self.props['finWidth'] = FloatProperty('Fin width', Unit.METER, 0, 1) + self.props['finLength'] = FloatProperty('Fin length', Unit.METER, 0, 1) + self.props['coreDiameter'] = FloatProperty('Core diameter', Unit.METER, 0, 1) def generateCoreMap(self): coreRadius = self.normalize(self.props['coreDiameter'].getValue()) / 2 @@ -42,9 +45,9 @@ def generateCoreMap(self): # Open up the fin self.coreMap[np.logical_and(vect, ends)] = 0 - def getDetailsString(self, lengthUnit='m'): - return 'Length: {}, Core: {}, Fins: {}'.format(self.props['length'].dispFormat(lengthUnit), - self.props['coreDiameter'].dispFormat(lengthUnit), + def getDetailsString(self, Unit=Unit.METER): + return 'Length: {}, Core: {}, Fins: {}'.format(self.props['length'].dispFormat(Unit), + self.props['coreDiameter'].dispFormat(Unit), self.props['numFins'].getValue()) def getGeometryErrors(self): diff --git a/motorlib/grains/moonBurner.py b/motorlib/grains/moonBurner.py index f51d87e..5da872c 100644 --- a/motorlib/grains/moonBurner.py +++ b/motorlib/grains/moonBurner.py @@ -1,16 +1,18 @@ """Moon burning grain submodule""" - +from ..enums.simAlertLevel import SimAlertLevel +from ..enums.simAlertType import SimAlertType +from ..enums.unit import Unit from ..grain import FmmGrain from ..properties import FloatProperty -from ..simResult import SimAlert, SimAlertLevel, SimAlertType +from ..simResult import SimAlert class MoonBurner(FmmGrain): """A moonburner is very similar to a BATES grain except the core is off center by a specified distance.""" geomName = 'Moon Burner' def __init__(self): super().__init__() - self.props['coreOffset'] = FloatProperty('Core offset', 'm', 0, 1) - self.props['coreDiameter'] = FloatProperty('Core diameter', 'm', 0, 1) + self.props['coreOffset'] = FloatProperty('Core offset', Unit.METER, 0, 1) + self.props['coreDiameter'] = FloatProperty('Core diameter', Unit.METER, 0, 1) def generateCoreMap(self): coreRadius = self.normalize(self.props['coreDiameter'].getValue()) / 2 @@ -19,9 +21,9 @@ def generateCoreMap(self): # Open up core self.coreMap[(self.mapX - coreOffset)**2 + self.mapY**2 < coreRadius**2] = 0 - def getDetailsString(self, lengthUnit='m'): - return 'Length: {}, Core: {}'.format(self.props['length'].dispFormat(lengthUnit), - self.props['coreDiameter'].dispFormat(lengthUnit)) + def getDetailsString(self, Unit=Unit.METER): + return 'Length: {}, Core: {}'.format(self.props['length'].dispFormat(Unit), + self.props['coreDiameter'].dispFormat(Unit)) def getGeometryErrors(self): errors = super().getGeometryErrors() diff --git a/motorlib/grains/rodTube.py b/motorlib/grains/rodTube.py index 03d0c63..7ee998f 100644 --- a/motorlib/grains/rodTube.py +++ b/motorlib/grains/rodTube.py @@ -4,9 +4,12 @@ import skfmm from skimage import measure +from ..enums.simAlertLevel import SimAlertLevel +from ..enums.simAlertType import SimAlertType +from ..enums.unit import Unit from ..grain import PerforatedGrain from .. import geometry -from ..simResult import SimAlert, SimAlertLevel, SimAlertType +from ..simResult import SimAlert from ..properties import FloatProperty class RodTubeGrain(PerforatedGrain): @@ -15,9 +18,9 @@ class RodTubeGrain(PerforatedGrain): geomName = "Rod and Tube" def __init__(self): super().__init__() - self.props['coreDiameter'] = FloatProperty('Core Diameter', 'm', 0, 1) - self.props['rodDiameter'] = FloatProperty('Rod Diameter', 'm', 0, 1) - self.props['supportDiameter'] = FloatProperty('Support Diameter', 'm', 0, 1) + self.props['coreDiameter'] = FloatProperty('Core Diameter', Unit.METER, 0, 1) + self.props['rodDiameter'] = FloatProperty('Rod Diameter', Unit.METER, 0, 1) + self.props['supportDiameter'] = FloatProperty('Support Diameter', Unit.METER, 0, 1) self.tubeWeb = None self.rodWeb = None @@ -52,10 +55,10 @@ def getFaceArea(self, regDist): rodArea = 0 return tubeArea + rodArea - def getDetailsString(self, lengthUnit='m'): - return 'Length: {}, Core: {}, Rod: {}'.format(self.props['length'].dispFormat(lengthUnit), - self.props['coreDiameter'].dispFormat(lengthUnit), - self.props['rodDiameter'].dispFormat(lengthUnit)) + def getDetailsString(self, Unit=Unit.METER): + return 'Length: {}, Core: {}, Rod: {}'.format(self.props['length'].dispFormat(Unit), + self.props['coreDiameter'].dispFormat(Unit), + self.props['rodDiameter'].dispFormat(Unit)) def getGeometryErrors(self): errors = super().getGeometryErrors() diff --git a/motorlib/grains/star.py b/motorlib/grains/star.py index ccf87e8..1bcb647 100644 --- a/motorlib/grains/star.py +++ b/motorlib/grains/star.py @@ -2,9 +2,12 @@ import numpy as np +from ..enums.simAlertLevel import SimAlertLevel +from ..enums.simAlertType import SimAlertType +from ..enums.unit import Unit from ..grain import FmmGrain from ..properties import IntProperty, FloatProperty -from ..simResult import SimAlert, SimAlertLevel, SimAlertType +from ..simResult import SimAlert class StarGrain(FmmGrain): """A star grain has a core shaped like a star.""" @@ -12,8 +15,8 @@ class StarGrain(FmmGrain): def __init__(self): super().__init__() self.props['numPoints'] = IntProperty('Number of points', '', 0, 64) - self.props['pointLength'] = FloatProperty('Point length', 'm', 0, 1) - self.props['pointWidth'] = FloatProperty('Point base width', 'm', 0, 1) + self.props['pointLength'] = FloatProperty('Point length', Unit.METER, 0, 1) + self.props['pointWidth'] = FloatProperty('Point base width', Unit.METER, 0, 1) def generateCoreMap(self): numPoints = self.props['numPoints'].getValue() @@ -31,8 +34,8 @@ def generateCoreMap(self): near = comp1*self.mapX - comp0*self.mapY > -0.025 self.coreMap[np.logical_and(vect, near)] = 0 - def getDetailsString(self, lengthUnit='m'): - return 'Length: {}, Points: {}'.format(self.props['length'].dispFormat(lengthUnit), + def getDetailsString(self, Unit=Unit.METER): + return 'Length: {}, Points: {}'.format(self.props['length'].dispFormat(Unit), self.props['numPoints'].getValue()) def getGeometryErrors(self): diff --git a/motorlib/grains/xCore.py b/motorlib/grains/xCore.py index 7e56d14..f24e7da 100644 --- a/motorlib/grains/xCore.py +++ b/motorlib/grains/xCore.py @@ -2,17 +2,20 @@ import numpy as np +from ..enums.simAlertLevel import SimAlertLevel +from ..enums.simAlertType import SimAlertType +from ..enums.unit import Unit from ..grain import FmmGrain from ..properties import FloatProperty -from ..simResult import SimAlert, SimAlertLevel, SimAlertType +from ..simResult import SimAlert class XCore(FmmGrain): """An X Core grain has a core shaped like a plus sign or an X.""" geomName = 'X Core' def __init__(self): super().__init__() - self.props['slotWidth'] = FloatProperty('Slot width', 'm', 0, 1) - self.props['slotLength'] = FloatProperty('Slot length', 'm', 0, 1) + self.props['slotWidth'] = FloatProperty('Slot width', Unit.METER, 0, 1) + self.props['slotLength'] = FloatProperty('Slot length', Unit.METER, 0, 1) def generateCoreMap(self): slotWidth = self.normalize(self.props['slotWidth'].getValue()) @@ -21,10 +24,10 @@ def generateCoreMap(self): self.coreMap[np.logical_and(np.abs(self.mapY) < slotWidth/2, np.abs(self.mapX) < slotLength)] = 0 self.coreMap[np.logical_and(np.abs(self.mapX) < slotWidth/2, np.abs(self.mapY) < slotLength)] = 0 - def getDetailsString(self, lengthUnit='m'): - return 'Length: {}, Slots: {} by {}'.format(self.props['length'].dispFormat(lengthUnit), - self.props['slotWidth'].dispFormat(lengthUnit), - self.props['slotLength'].dispFormat(lengthUnit)) + def getDetailsString(self, Unit=Unit.METER): + return 'Length: {}, Slots: {} by {}'.format(self.props['length'].dispFormat(Unit), + self.props['slotWidth'].dispFormat(Unit), + self.props['slotLength'].dispFormat(Unit)) def getGeometryErrors(self): errors = super().getGeometryErrors() diff --git a/motorlib/motor.py b/motorlib/motor.py index 220d9a5..d04ab5c 100644 --- a/motorlib/motor.py +++ b/motorlib/motor.py @@ -1,9 +1,14 @@ """Conains the motor class and a supporting configuration property collection.""" +from .enums.multiValueChannels import MultiValueChannels +from .enums.simAlertLevel import SimAlertLevel +from .enums.simAlertType import SimAlertType +from .enums.singleValueChannels import SingleValueChannels +from .enums.unit import Unit from .grains import grainTypes from .nozzle import Nozzle from .propellant import Propellant from . import geometry -from .simResult import SimulationResult, SimAlert, SimAlertLevel, SimAlertType +from .simResult import SimulationResult, SimAlert from .grains import EndBurningGrain from .properties import PropertyCollection, FloatProperty, IntProperty from .constants import gasConstant @@ -14,15 +19,15 @@ class MotorConfig(PropertyCollection): def __init__(self): super().__init__() # Limits - self.props['maxPressure'] = FloatProperty('Maximum Allowed Pressure', 'Pa', 0, 7e7) - self.props['maxMassFlux'] = FloatProperty('Maximum Allowed Mass Flux', 'kg/(m^2*s)', 0, 1e4) + self.props['maxPressure'] = FloatProperty('Maximum Allowed Pressure', Unit.PASCAL, 0, 7e7) + self.props['maxMassFlux'] = FloatProperty('Maximum Allowed Mass Flux', Unit.KILOGRAM_PER_SQUARE_METER_PER_SECOND, 0, 1e4) self.props['minPortThroat'] = FloatProperty('Minimum Allowed Port/Throat Ratio', '', 1, 4) self.props['flowSeparationWarnPercent'] = FloatProperty('Flow Separation Warning Threshold', '', 0.00, 1) # Simulation - self.props['burnoutWebThres'] = FloatProperty('Web Burnout Threshold', 'm', 2.54e-5, 3.175e-3) + self.props['burnoutWebThres'] = FloatProperty('Web Burnout Threshold', Unit.METER, 2.54e-5, 3.175e-3) self.props['burnoutThrustThres'] = FloatProperty('Thrust Burnout Threshold', '%', 0.01, 10) - self.props['timestep'] = FloatProperty('Simulation Timestep', 's', 0.0001, 0.1) - self.props['ambPressure'] = FloatProperty('Ambient Pressure', 'Pa', 0.0001, 102000) + self.props['timestep'] = FloatProperty('Simulation Timestep', Unit.SECOND, 0.0001, 0.1) + self.props['ambPressure'] = FloatProperty('Ambient Pressure', Unit.PASCAL, 0.0001, 102000) self.props['mapDim'] = IntProperty('Grain Map Dimension', '', 250, 2000) self.props['sepPressureRatio'] = FloatProperty('Separation Pressure Ratio', '', 0.001, 1) @@ -184,18 +189,18 @@ def runSimulation(self, callback=None): 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)) - simRes.channels['pressure'].addData(self.calcIdealPressure(perGrainReg, 0, None)) - simRes.channels['force'].addData(0) - simRes.channels['mass'].addData([grain.getVolumeAtRegression(0) * density for grain in self.grains]) - simRes.channels['volumeLoading'].addData(100 * (1 - (self.calcFreeVolume(perGrainReg) / motorVolume))) - 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) + simRes.channels[SingleValueChannels.TIME].addData(0) + simRes.channels[SingleValueChannels.KN].addData(self.calcKN(perGrainReg, 0)) + simRes.channels[SingleValueChannels.PRESSURE].addData(self.calcIdealPressure(perGrainReg, 0, None)) + simRes.channels[SingleValueChannels.FORCE].addData(0) + simRes.channels[MultiValueChannels.MASS].addData([grain.getVolumeAtRegression(0) * density for grain in self.grains]) + simRes.channels[SingleValueChannels.VOLUME_LOADING].addData(100 * (1 - (self.calcFreeVolume(perGrainReg) / motorVolume))) + simRes.channels[MultiValueChannels.MASS_FLOW].addData([0 for grain in self.grains]) + simRes.channels[MultiValueChannels.MASS_FLUX].addData([0 for grain in self.grains]) + simRes.channels[MultiValueChannels.REGRESSION].addData([0 for grains in self.grains]) + simRes.channels[MultiValueChannels.WEB].addData([grain.getWebLeft(0) for grain in self.grains]) + simRes.channels[SingleValueChannels.EXIT_PRESSURE].addData(0) + simRes.channels[SingleValueChannels.D_THROAT].addData(0) # Check port/throat ratio and add a warning if it is large enough aftPort = self.grains[-1].getPortArea(0) @@ -217,44 +222,44 @@ def runSimulation(self, callback=None): 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()) + reg = dTime * self.propellant.getBurnRate(simRes.channels[SingleValueChannels.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 + massFlow += (simRes.channels[MultiValueChannels.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[MultiValueChannels.REGRESSION].addData(perGrainReg[:]) + simRes.channels[MultiValueChannels.WEB].addData(perGrainWeb) - simRes.channels['volumeLoading'].addData(100 * (1 - (self.calcFreeVolume(perGrainReg) / motorVolume))) - simRes.channels['mass'].addData(perGrainMass) - simRes.channels['massFlow'].addData(perGrainMassFlow) - simRes.channels['massFlux'].addData(perGrainMassFlux) + simRes.channels[SingleValueChannels.VOLUME_LOADING].addData(100 * (1 - (self.calcFreeVolume(perGrainReg) / motorVolume))) + simRes.channels[MultiValueChannels.MASS].addData(perGrainMass) + simRes.channels[MultiValueChannels.MASS_FLOW].addData(perGrainMassFlow) + simRes.channels[MultiValueChannels.MASS_FLUX].addData(perGrainMassFlux) # Calculate KN - dThroat = simRes.channels['dThroat'].getLast() - simRes.channels['kn'].addData(self.calcKN(perGrainReg, dThroat)) + dThroat = simRes.channels[SingleValueChannels.D_THROAT].getLast() + simRes.channels[SingleValueChannels.KN].addData(self.calcKN(perGrainReg, dThroat)) # Calculate Pressure - lastKn = simRes.channels['kn'].getLast() + lastKn = simRes.channels[SingleValueChannels.KN].getLast() pressure = self.calcIdealPressure(perGrainReg, dThroat, lastKn) - simRes.channels['pressure'].addData(pressure) + simRes.channels[SingleValueChannels.PRESSURE].addData(pressure) # Calculate Exit Pressure _, _, gamma, _, _ = self.propellant.getCombustionProperties(pressure) exitPressure = self.nozzle.getExitPressure(gamma, pressure) - simRes.channels['exitPressure'].addData(exitPressure) + simRes.channels[SingleValueChannels.EXIT_PRESSURE].addData(exitPressure) # Calculate force - force = self.calcForce(simRes.channels['pressure'].getLast(), dThroat, exitPressure) - simRes.channels['force'].addData(force) + force = self.calcForce(simRes.channels[SingleValueChannels.PRESSURE].getLast(), dThroat, exitPressure) + simRes.channels[SingleValueChannels.FORCE].addData(force) - simRes.channels['time'].addData(simRes.channels['time'].getLast() + dTime) + simRes.channels[SingleValueChannels.TIME].addData(simRes.channels[SingleValueChannels.TIME].getLast() + dTime) # Calculate any slag deposition or erosion of the throat if pressure == 0: @@ -263,7 +268,7 @@ def runSimulation(self, callback=None): 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) + simRes.channels[SingleValueChannels.D_THROAT].addData(dThroat + change) if callback is not None: # Uses the grain with the largest percentage of its web left @@ -290,7 +295,7 @@ def runSimulation(self, callback=None): # 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(): + for pressure in simRes.channels[SingleValueChannels.PRESSURE].getData(): if pressure > 0: err = self.propellant.getPressureErrors(pressure) if len(err) > 0: diff --git a/motorlib/nozzle.py b/motorlib/nozzle.py index b43218f..2abd31d 100644 --- a/motorlib/nozzle.py +++ b/motorlib/nozzle.py @@ -3,9 +3,12 @@ from scipy.optimize import fsolve +from .enums.simAlertLevel import SimAlertLevel +from .enums.simAlertType import SimAlertType +from .enums.unit import Unit from .properties import FloatProperty, PropertyCollection from . import geometry -from .simResult import SimAlert, SimAlertLevel, SimAlertType +from .simResult import SimAlert def eRatioFromPRatio(k, pRatio): @@ -16,18 +19,18 @@ class Nozzle(PropertyCollection): """An object that contains the details about a motor's nozzle.""" def __init__(self): super().__init__() - self.props['throat'] = FloatProperty('Throat Diameter', 'm', 0, 0.5) - self.props['exit'] = FloatProperty('Exit Diameter', 'm', 0, 1) + self.props['throat'] = FloatProperty('Throat Diameter', Unit.METER, 0, 0.5) + self.props['exit'] = FloatProperty('Exit Diameter', Unit.METER, 0, 1) self.props['efficiency'] = FloatProperty('Efficiency', '', 0, 2) - self.props['divAngle'] = FloatProperty('Divergence Half Angle', 'deg', 0, 90) - self.props['convAngle'] = FloatProperty('Convergence Half Angle', 'deg', 0, 90) - self.props['throatLength'] = FloatProperty('Throat Length', 'm', 0, 0.5) - self.props['slagCoeff'] = FloatProperty('Slag Buildup Coefficient', '(m*Pa)/s', 0, 1e6) - self.props['erosionCoeff'] = FloatProperty('Throat Erosion Coefficient', 'm/(s*Pa)', 0, 1e6) + self.props['divAngle'] = FloatProperty('Divergence Half Angle', Unit.DEGREES, 0, 90) + self.props['convAngle'] = FloatProperty('Convergence Half Angle', Unit.DEGREES, 0, 90) + self.props['throatLength'] = FloatProperty('Throat Length', Unit.METER, 0, 0.5) + self.props['slagCoeff'] = FloatProperty('Slag Buildup Coefficient', Unit.METER_PASCAL_PER_SECOND, 0, 1e6) + self.props['erosionCoeff'] = FloatProperty('Throat Erosion Coefficient', Unit.METER_PER_SECOND_PASCAL, 0, 1e6) - def getDetailsString(self, lengthUnit='m'): + def getDetailsString(self, Unit=Unit.METER): """Returns a human-readable string containing some details about the nozzle.""" - return 'Throat: {}'.format(self.props['throat'].dispFormat(lengthUnit)) + return 'Throat: {}'.format(self.props['throat'].dispFormat(Unit)) def calcExpansion(self): """Returns the nozzle's expansion ratio.""" diff --git a/motorlib/propellant.py b/motorlib/propellant.py index 5bcfc08..0c65b20 100644 --- a/motorlib/propellant.py +++ b/motorlib/propellant.py @@ -1,20 +1,22 @@ """Propellant submodule that contains the propellant class.""" - +from .enums.simAlertLevel import SimAlertLevel +from .enums.simAlertType import SimAlertType +from .enums.unit import Unit from .properties import PropertyCollection, FloatProperty, StringProperty, TabularProperty -from .simResult import SimAlert, SimAlertLevel, SimAlertType +from .simResult import SimAlert from .constants import gasConstant class PropellantTab(PropertyCollection): """Contains the combustion properties of a propellant over a specified pressure range.""" def __init__(self, tabDict=None): super().__init__() - self.props['minPressure'] = FloatProperty('Minimum Pressure', 'Pa', 0, 7e7) - self.props['maxPressure'] = FloatProperty('Maximum Pressure', 'Pa', 0, 7e7) - self.props['a'] = FloatProperty('Burn rate Coefficient', 'm/(s*Pa^n)', 1E-8, 2) + self.props['minPressure'] = FloatProperty('Minimum Pressure', Unit.PASCAL, 0, 7e7) + self.props['maxPressure'] = FloatProperty('Maximum Pressure', Unit.PASCAL, 0, 7e7) + self.props['a'] = FloatProperty('Burn rate Coefficient', Unit.METER_PER_SECOND_PASCAL_TO_THE_POWER_OF_N, 1E-8, 2) self.props['n'] = FloatProperty('Burn rate Exponent', '', -0.99, 0.99) self.props['k'] = FloatProperty('Specific Heat Ratio', '', 1+1e-6, 10) - self.props['t'] = FloatProperty('Combustion Temperature', 'K', 1, 10000) - self.props['m'] = FloatProperty('Exhaust Molar Mass', 'g/mol', 1e-6, 100) + self.props['t'] = FloatProperty('Combustion Temperature', Unit.KELVIN, 1, 10000) + self.props['m'] = FloatProperty('Exhaust Molar Mass', Unit.GRAM_PER_MOLE, 1e-6, 100) if tabDict is not None: self.setProperties(tabDict) @@ -24,7 +26,7 @@ class Propellant(PropertyCollection): def __init__(self, propDict=None): super().__init__() self.props['name'] = StringProperty('Name') - self.props['density'] = FloatProperty('Density', 'kg/m^3', 1, 10000) + self.props['density'] = FloatProperty('Density', Unit.KILOGRAM_PER_CUBIC_METER, 1, 10000) self.props['tabs'] = TabularProperty('Properties', PropellantTab) if propDict is not None: self.setProperties(propDict) diff --git a/motorlib/simResult.py b/motorlib/simResult.py index 2d7d81c..cc6f440 100644 --- a/motorlib/simResult.py +++ b/motorlib/simResult.py @@ -2,34 +2,12 @@ the channels and components that it is comprised of.""" import math -from enum import Enum - from . import geometry from . import units +from .enums.multiValueChannels import MultiValueChannels +from .enums.singleValueChannels import SingleValueChannels +from .enums.unit import Unit -class SimAlertLevel(Enum): - """Levels of severity for sim alerts""" - ERROR = 1 - WARNING = 2 - MESSAGE = 3 - -class SimAlertType(Enum): - """Types of sim alerts""" - GEOMETRY = 1 - CONSTRAINT = 2 - VALUE = 3 - -alertLevelNames = { - SimAlertLevel.ERROR: 'Error', - SimAlertLevel.WARNING: 'Warning', - SimAlertLevel.MESSAGE: 'Message' -} - -alertTypeNames = { - SimAlertType.GEOMETRY: 'Geometry', - SimAlertType.CONSTRAINT: 'Constraint', - SimAlertType.VALUE: 'Value' -} class SimAlert(): """A sim alert signifies a possible problem with a motor. It has levels of severity including 'error' (simulation @@ -37,6 +15,7 @@ class SimAlert(): (other information). The type describes the variety of issue the alert is associated with, and the description is a human-readable version string with more details about the problem. The location can either be None or a string to help the user find the problem.""" + def __init__(self, level, alertType, description, location=None): self.level = level self.type = alertType @@ -48,6 +27,7 @@ class LogChannel(): """A log channel accepts data from a single source throughout a simulation. It has a human-readable name such as 'Pressure' to help the user interpret the result, a value type that data passed in will be cast to, and a unit to aid in conversion and display. The data type can either be a scalar (float or int) or a list (list or tuple).""" + def __init__(self, name, valueType, unit): if valueType not in (int, float, list, tuple): raise TypeError('Value type not in allowed set') @@ -58,7 +38,7 @@ def __init__(self, name, valueType, unit): def getData(self, unit=None): """Return all of the data in the channel, converting it if a type is specified.""" - if unit is None: # No conversion needed + if unit is None: # No conversion needed return self.data if self.valueType in (list, tuple): @@ -90,7 +70,7 @@ def getMax(self): if self.valueType in (list, tuple): return max([max(l) for l in self.data]) return max(self.data) - + def getMin(self): """Returns the minimum value of all datapoints. For list datatypes, this operation finds the smallest single value in any list.""" @@ -98,13 +78,12 @@ def getMin(self): return min([min(l) for l in self.data]) return min(self.data) -singleValueChannels = ['time', 'kn', 'pressure', 'force', 'volumeLoading', 'exitPressure', 'dThroat'] -multiValueChannels = ['mass', 'massFlow', 'massFlux', 'regression', 'web'] class SimulationResult(): """A SimulationResult instance contains all results from a single simulation. It has a number of LogChannels, each capturing a single stream of outputs from the simulation. It also includes a flag of whether the simulation was considered a sucess, along with a list of alerts that the simulation produced while it was running.""" + def __init__(self, motor): self.motor = motor @@ -112,18 +91,18 @@ def __init__(self, motor): self.success = False self.channels = { - 'time': LogChannel('Time', float, 's'), - 'kn': LogChannel('Kn', float, ''), - 'pressure': LogChannel('Chamber Pressure', float, 'Pa'), - 'force': LogChannel('Thrust', float, 'N'), - 'mass': LogChannel('Propellant Mass', tuple, 'kg'), - 'volumeLoading': LogChannel('Volume Loading', float, '%'), - 'massFlow': LogChannel('Mass Flow', tuple, 'kg/s'), - 'massFlux': LogChannel('Mass Flux', tuple, 'kg/(m^2*s)'), - 'regression': LogChannel('Regression Depth', tuple, 'm'), - 'web': LogChannel('Web', tuple, 'm'), - 'exitPressure': LogChannel('Nozzle Exit Pressure', float, 'Pa'), - 'dThroat': LogChannel('Change in Throat Diameter', float, 'm') + SingleValueChannels.TIME: LogChannel('Time', float, Unit.SECOND), + SingleValueChannels.KN: LogChannel('Kn', float, ''), + SingleValueChannels.PRESSURE: LogChannel('Chamber Pressure', float, Unit.PASCAL), + SingleValueChannels.FORCE: LogChannel('Thrust', float, Unit.NEWTON), + MultiValueChannels.MASS: LogChannel('Propellant Mass', tuple, Unit.KILOGRAM), + SingleValueChannels.VOLUME_LOADING: LogChannel('Volume Loading', float, '%'), + MultiValueChannels.MASS_FLOW: LogChannel('Mass Flow', tuple, Unit.KILOGRAM_PER_SECOND), + MultiValueChannels.MASS_FLUX: LogChannel('Mass Flux', tuple, Unit.KILOGRAM_PER_SQUARE_METER_PER_SECOND), + MultiValueChannels.REGRESSION: LogChannel('Regression Depth', tuple, Unit.METER), + MultiValueChannels.WEB: LogChannel('Web', tuple, Unit.METER), + SingleValueChannels.EXIT_PRESSURE: LogChannel('Nozzle Exit Pressure', float, Unit.PASCAL), + SingleValueChannels.D_THROAT: LogChannel('Change in Throat Diameter', float, Unit.METER) } def addAlert(self, alert): @@ -133,29 +112,29 @@ def addAlert(self, alert): def getBurnTime(self): """Returns the burntime of the simulated motor, which is the time from the start when it was last producing thrust above the user's defined threshold.""" - return self.channels['time'].getLast() + return self.channels[SingleValueChannels.TIME].getLast() def getInitialKN(self): """Returns the motor's Kn before it started firing.""" - return self.channels['kn'].getPoint(0) + return self.channels[SingleValueChannels.KN].getPoint(0) def getPeakKN(self): """Returns the highest Kn that was observed during the motor's burn.""" - return self.channels['kn'].getMax() + return self.channels[SingleValueChannels.KN].getMax() def getAveragePressure(self): """Returns the average chamber pressure observed during the simulation.""" - return self.channels['pressure'].getAverage() + return self.channels[SingleValueChannels.PRESSURE].getAverage() def getMaxPressure(self): """Returns the highest chamber pressure that was observed during the motor's burn.""" - return self.channels['pressure'].getMax() - + return self.channels[SingleValueChannels.PRESSURE].getMax() + def getMinExitPressure(self): """Returns the lowest exit pressure that was observed during the motor's burn, ignoring startup and shutdown transients""" - exit_pressures = self.channels['exitPressure'].getData() + exit_pressures = self.channels[SingleValueChannels.EXIT_PRESSURE].getData() return min(exit_pressures) - + def getPercentBelowThreshold(self, channel, threshold): """Returns the total number of seconds spent below a given threshold value""" count = 0 @@ -163,28 +142,28 @@ def getPercentBelowThreshold(self, channel, threshold): for point in data: if point < threshold: count += 1 - return count/len(data) + return count / len(data) def getImpulse(self, stop=None): """Returns the impulse the simulated motor produced. If 'stop' is set to a value other than None, only the impulse to that point in the data is returned.""" impulse = 0 lastTime = 0 - for time, force in zip(self.channels['time'].data[:stop], self.channels['force'].data[:stop]): + for time, force in zip(self.channels[SingleValueChannels.TIME].data[:stop], self.channels[SingleValueChannels.FORCE].data[:stop]): impulse += force * (time - lastTime) lastTime = time return impulse def getAverageForce(self): """Returns the average force the motor produced during its burn.""" - return self.channels['force'].getAverage() + return self.channels[SingleValueChannels.FORCE].getAverage() def getDesignation(self): """Returns the standard amateur rocketry designation (H128, M1297) for the motor.""" imp = self.getImpulse() - if imp < 1.25: # This is to avoid a domain error finding log(0) + if imp < 1.25: # This is to avoid a domain error finding log(0) return 'N/A' - return chr(int(math.log(imp/1.25, 2)) + 65) + str(int(self.getAverageForce())) + return chr(int(math.log(imp / 1.25, 2)) + 65) + str(int(self.getAverageForce())) def getFullDesignation(self): """Returns the full motor designation, which also includes the total impulse prepended on""" @@ -192,13 +171,13 @@ def getFullDesignation(self): def getPeakMassFlux(self): """Returns the maximum mass flux observed at any grain end.""" - return self.channels['massFlux'].getMax() + return self.channels[MultiValueChannels.MASS_FLUX].getMax() def getPeakMassFluxLocation(self): """Returns the grain number at which the peak mass flux was observed.""" value = self.getPeakMassFlux() # Find the value to get the location - for frame in self.channels['massFlux'].getData(): + for frame in self.channels[MultiValueChannels.MASS_FLUX].getData(): if value in frame: return frame.index(value) return None @@ -227,12 +206,12 @@ def getPropellantLength(self): def getPropellantMass(self, index=0): """Returns the total mass of all propellant before the simulated burn. Optionally accepts a index that the mass will be sampled at.""" - return sum(self.channels['mass'].getPoint(index)) + return sum(self.channels[MultiValueChannels.MASS].getPoint(index)) def getVolumeLoading(self, index=0): """Returns the percentage of the motor's volume occupied by propellant. Optionally accepts a index that the value will be sampled at.""" - return self.channels['volumeLoading'].getPoint(index) + return self.channels[SingleValueChannels.VOLUME_LOADING].getPoint(index) def getIdealThrustCoefficient(self): """Returns the motor's thrust coefficient for the average pressure during the burn and no throat diameter @@ -261,10 +240,10 @@ def getAlertsByLevel(self, level): def shouldContinueSim(self, thrustThres): """Returns if the simulation should continue based on the thrust from the last timestep.""" # With only one data point, there is nothing to compare - if len(self.channels['time'].getData()) == 1: + if len(self.channels[SingleValueChannels.TIME].getData()) == 1: return True # Otherwise perform the comparison. 0.01 converts the threshold to a % - return self.channels['force'].getLast() > thrustThres * 0.01 * self.channels['force'].getMax() + return self.channels[SingleValueChannels.FORCE].getLast() > thrustThres * 0.01 * self.channels[SingleValueChannels.FORCE].getMax() def getCSV(self, pref=None, exclude=[], excludeGrains=[]): """Returns a string that contains a CSV of the simulated data. Preferences can be passed in to set units that @@ -295,16 +274,16 @@ def getCSV(self, pref=None, exclude=[], excludeGrains=[]): out += ';{}'.format(outUnits[chan]) out += '),' - out = out[:-1] # Remove the last comma + out = out[:-1] # Remove the last comma out += '\n' places = 5 - for ind, time in enumerate(self.channels['time'].getData()): + for ind, time in enumerate(self.channels[SingleValueChannels.TIME].getData()): out += str(round(time, places)) + ',' for chan in self.channels: if chan in exclude: continue - if chan != 'time': + if chan != SingleValueChannels.TIME: if self.channels[chan].valueType in (float, int): orig = self.channels[chan].getPoint(ind) conv = units.convert(orig, self.channels[chan].unit, outUnits[chan]) @@ -316,7 +295,7 @@ def getCSV(self, pref=None, exclude=[], excludeGrains=[]): conv = round(units.convert(grainVal, self.channels[chan].unit, outUnits[chan]), places) out += str(conv) + ',' - out = out[:-1] # Remove the last comma + out = out[:-1] # Remove the last comma out += '\n' return out diff --git a/motorlib/units.py b/motorlib/units.py index 4a067d2..0be8d99 100644 --- a/motorlib/units.py +++ b/motorlib/units.py @@ -1,67 +1,74 @@ """This module contains tables of units and their long form names, their conversion rates with other units, and functions for performing conversion.""" +from motorlib.enums.unit import Unit # The keys in this dictionary specify the units that all calculations are done in internally unitLabels = { - 'm': 'Length', - 'm^3': 'Volume', - 'm/s': 'Velocity', - 'N': 'Force', - 'Ns': 'Impulse', - 'Pa': 'Pressure', - 'kg': 'Mass', - 'kg/m^3': 'Density', - 'kg/s': 'Mass Flow', - 'kg/(m^2*s)': 'Mass Flux', - 'm/(s*Pa^n)': 'Burn Rate Coefficient', - '(m*Pa)/s': 'Nozzle Slag Coefficient', - 'm/(s*Pa)': 'Nozzle Erosion Coefficient' + Unit.METER: 'Length', + Unit.CUBIC_METER: 'Volume', + Unit.METER_PER_SECOND: 'Velocity', + Unit.NEWTON: 'Force', + Unit.NEWTON_SECOND: 'Impulse', + Unit.PASCAL: 'Pressure', + Unit.KILOGRAM: 'Mass', + Unit.KILOGRAM_PER_CUBIC_METER: 'Density', + Unit.KILOGRAM_PER_SECOND: 'Mass Flow', + Unit.KILOGRAM_PER_SQUARE_METER_PER_SECOND: 'Mass Flux', + Unit.METER_PER_SECOND_PASCAL_TO_THE_POWER_OF_N: 'Burn Rate Coefficient', + Unit.METER_PASCAL_PER_SECOND: 'Nozzle Slag Coefficient', + Unit.METER_PER_SECOND_PASCAL: 'Nozzle Erosion Coefficient' } unitTable = [ - ('m', 'cm', 100), - ('m', 'mm', 1000), - ('m', 'in', 39.37), - ('m', 'ft', 3.28), + (Unit.METER, Unit.CENTIMETER, 100), + (Unit.METER, Unit.METER, 1000), + (Unit.METER, Unit.INCH, 39.37), + (Unit.METER, Unit.FOOT, 3.28), - ('m^3', 'cm^3', 100**3), - ('m^3', 'mm^3', 1000**3), - ('m^3', 'in^3', 39.37**3), - ('m^3', 'ft^3', 3.28**3), + (Unit.CUBIC_METER, Unit.CUBIC_CENTIMETER, 100 ** 3), + (Unit.CUBIC_METER, Unit.CUBIC_MILLIMETER, 1000 ** 3), + (Unit.CUBIC_METER, Unit.CUBIC_INCH, 39.37 ** 3), + (Unit.CUBIC_METER, Unit.CUBIC_FOOT, 3.28 ** 3), - ('m/s', 'cm/s', 100), - ('m/s', 'mm/s', 1000), - ('m/s', 'ft/s', 3.28), - ('m/s', 'in/s', 39.37), + (Unit.METER_PER_SECOND, Unit.CENTIMETER_PER_SECOND, 100), + (Unit.METER_PER_SECOND, Unit.MILLIMETER_PER_SECOND, 1000), + (Unit.METER_PER_SECOND, Unit.FOOT_PER_SECOND, 3.28), + (Unit.METER_PER_SECOND, Unit.INCH_PER_SECOND, 39.37), - ('N', 'lbf', 0.2248), + (Unit.NEWTON, Unit.POUND_FORCE, 0.2248), - ('Ns', 'lbfs', 0.2248), + (Unit.NEWTON_SECOND, Unit.POUND_FORCE_SECOND, 0.2248), - ('Pa', 'MPa', 1/1000000), - ('Pa', 'psi', 1/6895), + (Unit.PASCAL, Unit.MEGAPASCAL, 1 / 1000000), + (Unit.PASCAL, Unit.POUND_PER_SQUARE_INCH, 1 / 6895), - ('kg', 'g', 1000), - ('kg', 'lb', 2.205), - ('kg', 'oz', 2.205 * 16), + (Unit.KILOGRAM, Unit.GRAM, 1000), + (Unit.KILOGRAM, Unit.POUND, 2.205), + (Unit.KILOGRAM, Unit.OUNCE, 2.205 * 16), - ('kg/m^3', 'lb/in^3', 3.61273e-5), - ('kg/m^3', 'g/cm^3', 0.001), + (Unit.KILOGRAM_PER_CUBIC_METER, Unit.POUND_PER_CUBIC_INCH, 3.61273e-5), + (Unit.KILOGRAM_PER_CUBIC_METER, Unit.GRAM_PER_CUBIC_CENTIMETER, 0.001), - ('kg/s', 'lb/s', 2.205), - ('kg/s', 'g/s', 1000), + (Unit.KILOGRAM_PER_SECOND, Unit.POUND_PER_SECOND, 2.205), + (Unit.KILOGRAM_PER_SECOND, Unit.GRAM_PER_SECOND, 1000), - ('kg/(m^2*s)', 'lb/(in^2*s)', 0.001422), + (Unit.KILOGRAM_PER_SQUARE_METER_PER_SECOND, Unit.POUND_PER_SQUARE_INCH_PER_SECOND, 0.001422), - ('(m*Pa)/s', '(m*MPa)/s', 1000000), - ('(m*Pa)/s', '(in*psi)/s', 0.00571014715), + (Unit.METER_PASCAL_PER_SECOND, Unit.METER_MEGAPASCAL_PER_SECOND, 1000000), + (Unit.METER_PASCAL_PER_SECOND, Unit.INCH_POUND_PER_SQUARE_INCH_PER_SECOND, + 0.00571014715), - ('m/(s*Pa)', 'm/(s*MPa)', 1/1000000), - ('m/(s*Pa)', 'thou/(s*psi)', 271447138), + (Unit.METER_PER_SECOND_PASCAL, Unit.METER_PER_SECOND_MEGAPASCAL, + 1 / 1000000), + (Unit.METER_PER_SECOND_PASCAL, + Unit.THOUSANDTH_INCH_PER_SECOND_POUND_PER_SQUARE_INCH, 271447138), - ('m/(s*Pa^n)', 'in/(s*psi^n)', 39.37) # Ratio converts m/s to in/s. The pressure conversion must be done separately + (Unit.METER_PER_SECOND_PASCAL_TO_THE_POWER_OF_N, + Unit.INCH_PER_SECOND_POUND_PER_SQUARE_INCH_TO_THE_POWER_OF_N, 39.37) + # Ratio converts m/s to in/s. The pressure conversion must be done separately ] + def getAllConversions(unit): """Returns a list of all units that the passed unit can be converted to.""" allConversions = [unit] @@ -72,6 +79,7 @@ def getAllConversions(unit): allConversions.append(conversion[0]) return allConversions + def getConversion(originUnit, destUnit): """Returns the ratio to convert between the two units. If the conversion does not exist, an exception is raised.""" if originUnit == destUnit: @@ -80,18 +88,21 @@ def getConversion(originUnit, destUnit): if conversion[0] == originUnit and conversion[1] == destUnit: return conversion[2] if conversion[1] == originUnit and conversion[0] == destUnit: - return 1/conversion[2] + return 1 / conversion[2] raise KeyError("Cannot find conversion from <" + originUnit + "> to <" + destUnit + ">") + def convert(quantity, originUnit, destUnit): """Returns the value of 'quantity' when it is converted from 'originUnit' to 'destUnit'.""" return quantity * getConversion(originUnit, destUnit) + def convertAll(quantities, originUnit, destUnit): """Converts a list of values from 'originUnit' to 'destUnit'.""" convRate = getConversion(originUnit, destUnit) return [q * convRate for q in quantities] + def convFormat(quantity, originUnit, destUnit, places=3): """Takes in a quantity in originUnit, converts it to destUnit and outputs a rounded and formatted string that includes the unit appended to the end.""" diff --git a/test/compare.py b/test/compare.py index 7758790..789300d 100644 --- a/test/compare.py +++ b/test/compare.py @@ -1,11 +1,11 @@ import sys import os -import matplotlib import yaml import warnings import motorlib.motor -from uilib.fileIO import loadFile, fileTypes +from uilib.enums.fileType import FileType +from uilib.fileIO import loadFile separator = '-' * 65 @@ -30,7 +30,7 @@ def formatPercent(percent): def runSim(path): print('Loading motor from ' + path) - res = loadFile(path, fileTypes.MOTOR) + res = loadFile(path, FileType.MOTOR) if res is not None: motor = motorlib.motor.Motor(res) print('Simulating burn...') diff --git a/test/unit/grains/bates.py b/test/unit/grains/bates.py index 1c079ef..edb73a6 100644 --- a/test/unit/grains/bates.py +++ b/test/unit/grains/bates.py @@ -1,6 +1,8 @@ import unittest import motorlib.grains -from motorlib.simResult import SimAlertLevel, SimAlertType +from motorlib.enums.simAlertLevel import SimAlertLevel +from motorlib.enums.simAlertType import SimAlertType +from motorlib.enums.unit import Unit class BatesGrainMethods(unittest.TestCase): @@ -13,7 +15,7 @@ def test_getDetailsString(self): 'coreDiameter': 0.02 }) self.assertEqual(grain.getDetailsString(), 'Length: 0.1 m, Core: 0.02 m') - self.assertEqual(grain.getDetailsString('cm'), 'Length: 10 cm, Core: 2 cm') + self.assertEqual(grain.getDetailsString(Unit.CENTIMETER), 'Length: 10 cm, Core: 2 cm') def test_getGeometryErrors(self): grain = motorlib.grains.BatesGrain() diff --git a/test/unit/grains/conical.py b/test/unit/grains/conical.py index ddcbdd6..001c92c 100644 --- a/test/unit/grains/conical.py +++ b/test/unit/grains/conical.py @@ -1,5 +1,7 @@ import unittest import motorlib.grains +from motorlib.enums.inhibitedEnds import InhibitedEnds + class ConicalGrainMethods(unittest.TestCase): @@ -28,7 +30,7 @@ def test_getFrustumInfo(self): 'diameter': 0.01, 'forwardCoreDiameter': 0.0025, 'aftCoreDiameter': 0.002, - 'inhibitedEnds': 'Both' + 'inhibitedEnds': InhibitedEnds.BOTH } testGrain = motorlib.grains.ConicalGrain() @@ -55,7 +57,7 @@ def test_getSurfaceAreaAtRegression(self): 'diameter': 0.01, 'forwardCoreDiameter': 0.0025, 'aftCoreDiameter': 0.002, - 'inhibitedEnds': 'Both' + 'inhibitedEnds': InhibitedEnds.BOTH } forwardFaceArea = 7.36310778e-05 @@ -69,13 +71,13 @@ def test_getSurfaceAreaAtRegression(self): self.assertAlmostEqual(testGrain.getSurfaceAreaAtRegression(0.001), 0.0013351790867045452) # For when uninibited conical grains work: - """testGrain.setProperty('inhibitedEnds', 'Top') + """testGrain.setProperty('inhibitedEnds', InhibitedEnds.TOP) self.assertAlmostEqual(testGrain.getSurfaceAreaAtRegression(0), lateralArea + aftFaceArea) - testGrain.setProperty('inhibitedEnds', 'Bottom') + testGrain.setProperty('inhibitedEnds', InhibitedEnds.BOTTOM) self.assertAlmostEqual(testGrain.getSurfaceAreaAtRegression(0), lateralArea + forwardFaceArea) - testGrain.setProperty('inhibitedEnds', 'Neither') + testGrain.setProperty('inhibitedEnds', InhibitedEnds.NEITHER) self.assertAlmostEqual(testGrain.getSurfaceAreaAtRegression(0), lateralArea + forwardFaceArea + aftFaceArea)""" def test_getVolumeAtRegression(self): @@ -84,7 +86,7 @@ def test_getVolumeAtRegression(self): 'diameter': 0.01, 'forwardCoreDiameter': 0.0025, 'aftCoreDiameter': 0.002, - 'inhibitedEnds': 'Both' + 'inhibitedEnds': InhibitedEnds.BOTH } testGrain = motorlib.grains.ConicalGrain() @@ -100,7 +102,7 @@ def test_getWebLeft(self): 'diameter': 0.01, 'forwardCoreDiameter': 0.0025, 'aftCoreDiameter': 0.002, - 'inhibitedEnds': 'Both' + 'inhibitedEnds': InhibitedEnds.BOTH } testGrain = motorlib.grains.ConicalGrain() diff --git a/test/unit/motor.py b/test/unit/motor.py index b14cc7b..b945961 100644 --- a/test/unit/motor.py +++ b/test/unit/motor.py @@ -2,6 +2,8 @@ import motorlib.motor import motorlib.grains import motorlib.propellant +from motorlib.enums.inhibitedEnds import InhibitedEnds + class TestMotorMethods(unittest.TestCase): @@ -14,7 +16,7 @@ def test_calcKN(self): 'diameter': 0.083058, 'length': 0.1397, 'coreDiameter': 0.05, - 'inhibitedEnds': 'Neither' + 'inhibitedEnds': InhibitedEnds.NEITHER }) tm.grains.append(bg) @@ -34,7 +36,7 @@ def test_calcPressure(self): 'diameter': 0.083058, 'length': 0.1397, 'coreDiameter': 0.05, - 'inhibitedEnds': 'Neither' + 'inhibitedEnds': InhibitedEnds.NEITHER }) tm.grains.append(bg) diff --git a/uilib/converters/burnsimExporter.py b/uilib/converters/burnsimExporter.py index d404537..b3755b9 100644 --- a/uilib/converters/burnsimExporter.py +++ b/uilib/converters/burnsimExporter.py @@ -1,5 +1,8 @@ import xml.etree.ElementTree as ET +from motorlib.enums.inhibitedEnds import InhibitedEnds +from motorlib.enums.unit import Unit +from motorlib.enums.unit import Unit from motorlib.properties import PropertyCollection, FloatProperty, StringProperty import motorlib from ..converter import Exporter @@ -28,15 +31,16 @@ motorlib.grains.Finocyl: '7' } + def mToIn(value): """Converts a float containing meters to a string of inches""" - return str(motorlib.units.convert(value, 'm', 'in')) + return str(motorlib.units.convert(value, Unit.METER, Unit.INCH)) class BurnSimExporter(Exporter): def __init__(self, manager): super().__init__(manager, 'BurnSim File', - 'Exports the current motor for use in BurnSim 3.0', {'.bsx': 'BurnSim Files'}) + 'Exports the current motor for use in BurnSim 3.0', {'.bsx': 'BurnSim Files'}) self.reqNotMet = "Current motor must have a propellant set to export as a BurnSim file." def doConversion(self, path, config): @@ -66,9 +70,9 @@ def doConversion(self, path, config): outGrain.attrib['EndsInhibited'] = '1' else: ends = grain.getProperty('inhibitedEnds') - if ends == 'Neither': + if ends == InhibitedEnds.NEITHER: outGrain.attrib['EndsInhibited'] = '0' - elif ends in ('Top', 'Bottom'): + elif ends in (InhibitedEnds.TOP, InhibitedEnds.BOTTOM): outGrain.attrib['EndsInhibited'] = '1' else: outGrain.attrib['EndsInhibited'] = '2' @@ -101,14 +105,18 @@ def doConversion(self, path, config): # Have to pick a single pressure for output exportPressure = 5.17e6 ballA, ballN, gamma, _, m = motor.propellant.getCombustionProperties(exportPressure) - ballA = motorlib.units.convert(ballA * (6895**ballN), 'm/(s*Pa^n)', 'in/(s*psi^n)') + ballA = motorlib.units.convert(ballA * (6895 ** ballN), + + Unit.INCH_PER_SECOND_POUND_PER_SQUARE_INCH_TO_THE_POWER_OF_N) outProp.attrib['BallisticA'] = str(ballA) outProp.attrib['BallisticN'] = str(ballN) - density = str(motorlib.units.convert(motor.propellant.getProperty('density'), 'kg/m^3', 'lb/in^3')) + density = str(motorlib.units.convert(motor.propellant.getProperty('density'), + Unit.KILOGRAM_PER_CUBIC_METER, + Unit.POUND_PER_CUBIC_INCH)) outProp.attrib['Density'] = density outProp.attrib['SpecificHeatRatio'] = str(gamma) outProp.attrib['MolarMass'] = str(m) - outProp.attrib['CombustionTemp'] = '0' # Unclear if this is used anyway + outProp.attrib['CombustionTemp'] = '0' # Unclear if this is used anyway ispStar = motor.propellant.getCStar(exportPressure) / 9.80665 outProp.attrib['ISPStar'] = str(ispStar) # Add empty notes section diff --git a/uilib/converters/burnsimImporter.py b/uilib/converters/burnsimImporter.py index 8c93194..92d381b 100644 --- a/uilib/converters/burnsimImporter.py +++ b/uilib/converters/burnsimImporter.py @@ -1,6 +1,8 @@ import xml.etree.ElementTree as ET import motorlib +from motorlib.enums.inhibitedEnds import InhibitedEnds +from motorlib.enums.unit import Unit from ..converter import Importer @@ -21,21 +23,27 @@ '9': 'Pie Segment' } + def inToM(value): """Converts a string containing a value in inches to a float of meters""" - return motorlib.units.convert(float(value), 'in', 'm') + return motorlib.units.convert(float(value), Unit.INCH, Unit.METER) + def importPropellant(node): propellant = motorlib.propellant.Propellant() propTab = motorlib.propellant.PropellantTab() propellant.setProperty('name', node.attrib['Name']) ballN = float(node.attrib['BallisticN']) - ballA = float(node.attrib['BallisticA']) * 1/(6895**ballN) + ballA = float(node.attrib['BallisticA']) * 1 / (6895 ** ballN) propTab.setProperty('n', ballN) # Conversion only does in/s to m/s, the rest is handled above - ballA = motorlib.units.convert(ballA, 'in/(s*psi^n)', 'm/(s*Pa^n)') + ballA = motorlib.units.convert(ballA, + Unit.INCH_PER_SECOND_POUND_PER_SQUARE_INCH_TO_THE_POWER_OF_N, + Unit.METER_PER_SECOND_PASCAL_TO_THE_POWER_OF_N) propTab.setProperty('a', ballA) - density = motorlib.units.convert(float(node.attrib['Density']), 'lb/in^3', 'kg/m^3') + density = motorlib.units.convert(float(node.attrib['Density']), + Unit.POUND_PER_CUBIC_INCH, + Unit.KILOGRAM_PER_CUBIC_METER) propellant.setProperty('density', density) propTab.setProperty('k', float(node.attrib['SpecificHeatRatio'])) impMolarMass = node.attrib['MolarMass'] @@ -51,6 +59,7 @@ def importPropellant(node): propellant.setProperty('tabs', [propTab.getProperties()]) return propellant + class BurnSimImporter(Importer): def __init__(self, manager): super().__init__(manager, 'BurnSim Motor', 'Loads motor files for BurnSim 3.0', {'.bsx': 'BurnSim Files'}) @@ -81,34 +90,34 @@ def doConversion(self, path): grainType = child.attrib['Type'] if child.attrib['EndsInhibited'] == '1': - motor.grains[-1].setProperty('inhibitedEnds', 'Top') + motor.grains[-1].setProperty('inhibitedEnds', InhibitedEnds.TOP) elif child.attrib['EndsInhibited'] == '2': - motor.grains[-1].setProperty('inhibitedEnds', 'Both') + motor.grains[-1].setProperty('inhibitedEnds', InhibitedEnds.BOTH) - if grainType in ('1', '3', '7'): # Grains with core diameter + if grainType in ('1', '3', '7'): # Grains with core diameter motor.grains[-1].setProperty('coreDiameter', inToM(child.attrib['CoreDiameter'])) - if grainType == '2': # D grain specific properties + if grainType == '2': # D grain specific properties motor.grains[-1].setProperty('slotOffset', inToM(child.attrib['EdgeOffset'])) - elif grainType == '3': # Moonburner specific properties + elif grainType == '3': # Moonburner specific properties motor.grains[-1].setProperty('coreOffset', inToM(child.attrib['CoreOffset'])) - elif grainType == '5': # C grain specific properties + elif grainType == '5': # C grain specific properties motor.grains[-1].setProperty('slotWidth', inToM(child.attrib['SlotWidth'])) radius = motor.grains[-1].getProperty('diameter') / 2 motor.grains[-1].setProperty('slotOffset', radius - inToM(child.attrib['SlotDepth'])) - elif grainType == '6': # X core specific properties + elif grainType == '6': # X core specific properties motor.grains[-1].setProperty('slotWidth', inToM(child.attrib['SlotWidth'])) motor.grains[-1].setProperty('slotLength', inToM(child.attrib['CoreDiameter']) / 2) - elif grainType == '7': # Finocyl specific properties + elif grainType == '7': # Finocyl specific properties motor.grains[-1].setProperty('finWidth', inToM(child.attrib['FinWidth'])) motor.grains[-1].setProperty('finLength', inToM(child.attrib['FinLength'])) motor.grains[-1].setProperty('numFins', int(child.attrib['FinCount'])) - if not propSet: # Use propellant numbers from the forward grain + if not propSet: # Use propellant numbers from the forward grain motor.propellant = importPropellant(child.find('Propellant')) propSet = True diff --git a/uilib/converters/engExporter.py b/uilib/converters/engExporter.py index d5f02b3..4171c8b 100644 --- a/uilib/converters/engExporter.py +++ b/uilib/converters/engExporter.py @@ -1,21 +1,22 @@ -import xml.etree.ElementTree as ET -from PyQt5.QtWidgets import QDialog, QFileDialog, QDialogButtonBox, QApplication +from PyQt5.QtWidgets import QDialog, QApplication +from motorlib.enums.singleValueChannels import SingleValueChannels +from motorlib.enums.unit import Unit from motorlib.properties import PropertyCollection, FloatProperty, StringProperty, EnumProperty -import motorlib from ..converter import Exporter +from ..enums.fileAction import FileAction from ..views.EngExporter_ui import Ui_EngExporterDialog class EngSettings(PropertyCollection): def __init__(self): super().__init__() - self.props['diameter'] = FloatProperty('Motor Diameter', 'm', 0, 1) - self.props['length'] = FloatProperty('Motor Length', 'm', 0, 4) - self.props['hardwareMass'] = FloatProperty('Hardware Mass', 'kg', 0, 1000) + self.props['diameter'] = FloatProperty('Motor Diameter', Unit.METER, 0, 1) + self.props['length'] = FloatProperty('Motor Length', Unit.METER, 0, 4) + self.props['hardwareMass'] = FloatProperty('Hardware Mass', Unit.KILOGRAM, 0, 1000) self.props['designation'] = StringProperty('Motor Designation') self.props['manufacturer'] = StringProperty('Motor Manufacturer') - self.props['append'] = EnumProperty('Existing File', ['Append', 'Overwrite']) + self.props['append'] = EnumProperty('Existing File', [FileAction.APPEND, FileAction.OVERWRITE]) class EngExportMenu(QDialog): @@ -47,7 +48,7 @@ def __init__(self, manager): self.reqNotMet = "Must have run a simulation to export a .ENG file." def doConversion(self, path, config): - mode = 'a' if config['append'] == 'Append' else 'w' + mode = 'a' if config['append'] == FileAction.APPEND else 'w' with open(path, mode) as outFile: propMass = self.manager.simRes.getPropellantMass() contents = ' '.join([config['designation'], @@ -59,8 +60,8 @@ def doConversion(self, path, config): config['manufacturer'] ]) + '\n' - timeData = self.manager.simRes.channels['time'].getData() - forceData = self.manager.simRes.channels['force'].getData() + timeData = self.manager.simRes.channels[SingleValueChannels.TIME].getData() + forceData = self.manager.simRes.channels[SingleValueChannels.FORCE].getData() # Add on a 0-thrust datapoint right after the burn to satisfy RAS Aero if forceData[-1] != 0: timeData.append(self.manager.simRes.getBurnTime() + 0.01) diff --git a/uilib/converters/imageExporter.py b/uilib/converters/imageExporter.py index 506aac3..3f55ef7 100644 --- a/uilib/converters/imageExporter.py +++ b/uilib/converters/imageExporter.py @@ -1,6 +1,7 @@ from PyQt5.QtWidgets import QDialog, QApplication -from motorlib.simResult import singleValueChannels, multiValueChannels +from motorlib.enums.multiValueChannels import MultiValueChannels +from motorlib.enums.singleValueChannels import SingleValueChannels from ..converter import Exporter from ..views.ImageExporter_ui import Ui_ImageExporter @@ -17,9 +18,15 @@ def __init__(self, converter): def exec(self): self.ui.independent.resetChecks() - self.ui.independent.setupChecks(False, exclude=['kn', 'pressure', 'force', 'mass', - 'massFlow', 'massFlux', 'exitPressure', 'dThroat'], - default='time') + self.ui.independent.setupChecks(False, exclude=[SingleValueChannels.KN, + SingleValueChannels.PRESSURE, + SingleValueChannels.FORCE, + MultiValueChannels.MASS, + MultiValueChannels.MASS_FLOW, + MultiValueChannels.MASS_FLUX, + SingleValueChannels.EXIT_PRESSURE, + SingleValueChannels.D_THROAT], + default=SingleValueChannels.TIME) self.ui.independent.checksChanged.connect(self.validateChecks) self.ui.dependent.resetChecks() self.ui.dependent.setupChecks(True) @@ -33,11 +40,11 @@ def exec(self): return None def validateChecks(self): - if self.ui.independent.getSelectedChannels()[0] in multiValueChannels: - self.ui.dependent.unselect(singleValueChannels) - self.ui.dependent.toggleEnabled(singleValueChannels, False) + if self.ui.independent.getSelectedChannels()[0] in MultiValueChannels: + self.ui.dependent.unselect(SingleValueChannels) + self.ui.dependent.toggleEnabled(SingleValueChannels, False) else: - self.ui.dependent.toggleEnabled(singleValueChannels, True) + self.ui.dependent.toggleEnabled(SingleValueChannels, True) class ImageExporter(Exporter): diff --git a/uilib/defaults.py b/uilib/defaults.py index d7b27bf..4f948db 100644 --- a/uilib/defaults.py +++ b/uilib/defaults.py @@ -1,4 +1,5 @@ """Provides default properties for the UI.""" +from motorlib.enums.unit import Unit DEFAULT_PREFERENCES = { 'general': { @@ -15,17 +16,17 @@ 'flowSeparationWarnPercent': 0.05 }, 'units': { - 'm': 'in', - 'm^3': 'in^3', - 'm/s': 'ft/s', - 'Pa': 'psi', - 'kg': 'lb', - 'kg/m^3': 'lb/in^3', - 'kg/s': 'lb/s', - 'kg/(m^2*s)': 'lb/(in^2*s)', - '(m*Pa)/s': '(in*psi)/s', - 'm/(s*Pa)': 'thou/(s*psi)', - 'm/(s*Pa^n)': 'in/(s*psi^n)' + Unit.METER: Unit.INCH, + Unit.CUBIC_METER: Unit.CUBIC_INCH, + Unit.METER_PER_SECOND: Unit.FOOT_PER_SECOND, + Unit.PASCAL: Unit.POUND_PER_SQUARE_INCH, + Unit.KILOGRAM: Unit.POUND, + Unit.KILOGRAM_PER_CUBIC_METER: Unit.POUND_PER_CUBIC_INCH, + Unit.KILOGRAM_PER_SECOND:Unit.POUND_PER_SECOND, + Unit.KILOGRAM_PER_SQUARE_METER_PER_SECOND: Unit.POUND_PER_SQUARE_INCH_PER_SECOND, + Unit.METER_PASCAL_PER_SECOND: Unit.INCH_POUND_PER_SQUARE_INCH_PER_SECOND, + Unit.METER_PER_SECOND_PASCAL: Unit.THOUSANDTH_INCH_PER_SECOND_POUND_PER_SQUARE_INCH, + Unit.METER_PER_SECOND_PASCAL_TO_THE_POWER_OF_N: Unit.INCH_PER_SECOND_POUND_PER_SQUARE_INCH_TO_THE_POWER_OF_N } } diff --git a/uilib/enums/__init__.py b/uilib/enums/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uilib/enums/fileAction.py b/uilib/enums/fileAction.py new file mode 100644 index 0000000..5ce8298 --- /dev/null +++ b/uilib/enums/fileAction.py @@ -0,0 +1,8 @@ +from enum import Enum + + +# Python 3.11 supports `StrEnum` that would make this a bit more concise to write +# https://docs.python.org/3/library/enum.html#enum.StrEnum +class FileAction(str, Enum): + APPEND = 'Append' + OVERWRITE = 'Overwrite' diff --git a/uilib/enums/fileType.py b/uilib/enums/fileType.py new file mode 100644 index 0000000..8414754 --- /dev/null +++ b/uilib/enums/fileType.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class FileType(Enum): + PREFERENCES = 1 + PROPELLANTS = 2 + MOTOR = 3 diff --git a/uilib/fileIO.py b/uilib/fileIO.py index a5b94a0..9a4dd3c 100644 --- a/uilib/fileIO.py +++ b/uilib/fileIO.py @@ -1,22 +1,17 @@ -from enum import Enum import os -import platform from PyQt5.QtWidgets import QApplication import yaml import appdirs +from motorlib.enums.unit import Unit from .defaults import DEFAULT_PREFERENCES, DEFAULT_PROPELLANTS, KNSU_PROPS +from .enums.fileType import FileType from .logger import logger appVersion = (0, 6, 0) appVersionStr = '.'.join(map(str, appVersion)) -class fileTypes(Enum): - PREFERENCES = 1 - PROPELLANTS = 2 - MOTOR = 3 - def futureVersion(verA, verB): # Returns true if a is newer than b major = verA[0] > verB[0] minor = verA[0] == verB[0] and verA[1] > verB[1] @@ -34,6 +29,8 @@ def saveFile(path, data, dataType): yaml.dump(output, saveLocation) def loadFile(path, dataType): + fix_enum_refs(path) + with open(path, 'r') as readLocation: fileData = yaml.load(readLocation, Loader=yaml.Loader) @@ -55,7 +52,19 @@ def loadFile(path, dataType): # Otherwise it is from a past version and will be migrated return doMigration(fileData)['data'] - # Returns the path that files like preferences and propellant library should be in. Previously, all platforms except + +def fix_enum_refs(path): + # changes to v0.6.0 that introduced the new enums, make so that the old files break since some classes do not exist. + # as a fix we run this check beforehand and rewrite the relevant parts. In the future... use Feature Flags kids! + with open(path, 'r') as file: + content = file.read() + modified_content = content.replace("!!python/object/apply:uilib.fileIO.fileTypes", + "!!python/object/apply:uilib.enums.fileType.FileType") + with open(path, 'w') as file: + file.write(modified_content) + + +# Returns the path that files like preferences and propellant library should be in. Previously, all platforms except # Mac OS put these files alongside the executable, but the v0.5.0 added an installer for windows so it makes more # sense to use the user's data directory now. def getConfigPath(): @@ -68,7 +77,7 @@ def passthrough(data): return data -#0.5.0 to 0.6.0 +# 0.5.0 to 0.6.0 def migrateMotor_0_5_0_to_0_6_0(data): data['config']['sepPressureRatio'] = DEFAULT_PREFERENCES['general']['sepPressureRatio'] data['config']['flowSeparationWarnPercent'] = DEFAULT_PREFERENCES['general']['flowSeparationWarnPercent'] @@ -116,8 +125,8 @@ def tabularizePropellant(data): def migratePref_0_3_0_to_0_4_0(data): data['general']['igniterPressure'] = DEFAULT_PREFERENCES['general']['igniterPressure'] - data['units']['(m*Pa)/s'] = '(in*psi)/s' - data['units']['m/(s*Pa)'] = 'thou/(s*psi)' + data['units'][Unit.METER_PASCAL_PER_SECOND] = Unit.INCH_POUND_PER_SQUARE_INCH_PER_SECOND + data['units'][Unit.METER_PER_SECOND_PASCAL] = Unit.THOUSANDTH_INCH_PER_SECOND_POUND_PER_SQUARE_INCH return data def migrateProp_0_3_0_to_0_4_0(data): @@ -157,33 +166,33 @@ def migrateMotor_0_2_0_to_0_3_0(data): migrations = { (0, 5, 0): { 'to': (0, 6, 0), - fileTypes.PREFERENCES: passthrough, - fileTypes.PROPELLANTS: passthrough, - fileTypes.MOTOR: migrateMotor_0_5_0_to_0_6_0 + FileType.PREFERENCES: passthrough, + FileType.PROPELLANTS: passthrough, + FileType.MOTOR: migrateMotor_0_5_0_to_0_6_0 }, (0, 4, 0): { 'to': (0, 5, 0), - fileTypes.PREFERENCES: migratePref_0_4_0_to_0_5_0, - fileTypes.PROPELLANTS: migrateProp_0_4_0_to_0_5_0, - fileTypes.MOTOR: migrateMotor_0_4_0_to_0_5_0, + FileType.PREFERENCES: migratePref_0_4_0_to_0_5_0, + FileType.PROPELLANTS: migrateProp_0_4_0_to_0_5_0, + FileType.MOTOR: migrateMotor_0_4_0_to_0_5_0, }, (0, 3, 0): { 'to': (0, 4, 0), - fileTypes.PREFERENCES: migratePref_0_3_0_to_0_4_0, - fileTypes.PROPELLANTS: migrateProp_0_3_0_to_0_4_0, - fileTypes.MOTOR: migrateMotor_0_3_0_to_0_4_0 + FileType.PREFERENCES: migratePref_0_3_0_to_0_4_0, + FileType.PROPELLANTS: migrateProp_0_3_0_to_0_4_0, + FileType.MOTOR: migrateMotor_0_3_0_to_0_4_0 }, (0, 2, 0): { 'to': (0, 3, 0), - fileTypes.PREFERENCES: migratePref_0_2_0_to_0_3_0, - fileTypes.PROPELLANTS: passthrough, - fileTypes.MOTOR: migrateMotor_0_2_0_to_0_3_0 + FileType.PREFERENCES: migratePref_0_2_0_to_0_3_0, + FileType.PROPELLANTS: passthrough, + FileType.MOTOR: migrateMotor_0_2_0_to_0_3_0 }, (0, 1, 0): { 'to': (0, 2, 0), - fileTypes.PREFERENCES: passthrough, - fileTypes.PROPELLANTS: passthrough, - fileTypes.MOTOR: passthrough + FileType.PREFERENCES: passthrough, + FileType.PROPELLANTS: passthrough, + FileType.MOTOR: passthrough } } diff --git a/uilib/fileManager.py b/uilib/fileManager.py index e45337f..434853a 100644 --- a/uilib/fileManager.py +++ b/uilib/fileManager.py @@ -3,8 +3,9 @@ from PyQt5.QtCore import pyqtSignal import motorlib +from .enums.fileType import FileType -from .fileIO import saveFile, loadFile, fileTypes +from .fileIO import saveFile, loadFile from .helpers import FLAGS_NO_ICON from .logger import logger @@ -55,7 +56,7 @@ def save(self): self.saveAs() else: try: - saveFile(self.fileName, self.fileHistory[self.currentVersion], fileTypes.MOTOR) + saveFile(self.fileName, self.fileHistory[self.currentVersion], FileType.MOTOR) self.savedVersion = self.currentVersion self.sendTitleUpdate() except Exception as exc: @@ -75,7 +76,7 @@ def load(self, path=None): path = QFileDialog.getOpenFileName(None, 'Load motor', '', 'Motor Files (*.ric)')[0] if path != '': # If they cancel the dialog, path will be an empty string try: - res = loadFile(path, fileTypes.MOTOR) + res = loadFile(path, FileType.MOTOR) if res is not None: motor = motorlib.motor.Motor() motor.applyDict(res) diff --git a/uilib/preferencesManager.py b/uilib/preferencesManager.py index b9734cc..a27b40b 100644 --- a/uilib/preferencesManager.py +++ b/uilib/preferencesManager.py @@ -1,10 +1,11 @@ from PyQt5.QtCore import QObject, pyqtSignal -from motorlib.properties import PropertyCollection, FloatProperty, IntProperty, EnumProperty +from motorlib.properties import PropertyCollection, EnumProperty from motorlib.units import unitLabels, getAllConversions from motorlib.motor import MotorConfig +from .enums.fileType import FileType -from .fileIO import loadFile, saveFile, getConfigPath, fileTypes +from .fileIO import loadFile, saveFile, getConfigPath from .defaults import DEFAULT_PREFERENCES from .widgets import preferencesMenu from .logger import logger @@ -55,7 +56,7 @@ def newPreferences(self, prefDict): def loadPreferences(self): try: - prefDict = loadFile(getConfigPath() + 'preferences.yaml', fileTypes.PREFERENCES) + prefDict = loadFile(getConfigPath() + 'preferences.yaml', FileType.PREFERENCES) self.preferences.applyDict(prefDict) self.publishPreferences() except FileNotFoundError: @@ -65,7 +66,7 @@ def loadPreferences(self): def savePreferences(self): try: logger.log('Saving preferences to "{}"'.format(getConfigPath() + 'preferences.yaml')) - saveFile(getConfigPath() + 'preferences.yaml', self.preferences.getDict(), fileTypes.PREFERENCES) + saveFile(getConfigPath() + 'preferences.yaml', self.preferences.getDict(), FileType.PREFERENCES) except: logger.warn('Unable to save preferences') diff --git a/uilib/propellantManager.py b/uilib/propellantManager.py index 1da0d47..8608028 100644 --- a/uilib/propellantManager.py +++ b/uilib/propellantManager.py @@ -3,12 +3,13 @@ import motorlib from .defaults import DEFAULT_PROPELLANTS -from .fileIO import loadFile, saveFile, fileTypes, getConfigPath +from .enums.fileType import FileType +from .fileIO import loadFile, saveFile, getConfigPath from .widgets.propellantMenu import PropellantMenu from .logger import logger -class PropellantManager(QObject): +class PropellantManager(QObject): updated = pyqtSignal() def __init__(self): @@ -21,7 +22,7 @@ def __init__(self): def loadPropellants(self): try: - propList = loadFile(getConfigPath() + 'propellants.yaml', fileTypes.PROPELLANTS) + propList = loadFile(getConfigPath() + 'propellants.yaml', FileType.PROPELLANTS) for propDict in propList: newProp = motorlib.propellant.Propellant() newProp.setProperties(propDict) @@ -35,7 +36,7 @@ def savePropellants(self): propellants = [prop.getProperties() for prop in self.propellants] try: logger.log('Saving propellants to "{}"'.format(getConfigPath() + 'propellants.yaml')) - saveFile(getConfigPath() + 'propellants.yaml', propellants, fileTypes.PROPELLANTS) + saveFile(getConfigPath() + 'propellants.yaml', propellants, FileType.PROPELLANTS) except: logger.warn('Unable to save propellants!') diff --git a/uilib/tools/changeDiameter.py b/uilib/tools/changeDiameter.py index df31bc8..0c5c596 100644 --- a/uilib/tools/changeDiameter.py +++ b/uilib/tools/changeDiameter.py @@ -1,11 +1,12 @@ import motorlib +from motorlib.enums.unit import Unit from ..tool import Tool class ChangeDiameterTool(Tool): def __init__(self, manager): - props = {'diameter': motorlib.properties.FloatProperty('Diameter', 'm', 0, 1)} + props = {'diameter': motorlib.properties.FloatProperty('Diameter', Unit.METER, 0, 1)} super().__init__(manager, 'Motor diameter', 'Use this tool to set the diameter of all grains in the motor.', diff --git a/uilib/tools/neutralBates.py b/uilib/tools/neutralBates.py index d7b771d..2ce7865 100644 --- a/uilib/tools/neutralBates.py +++ b/uilib/tools/neutralBates.py @@ -1,13 +1,14 @@ import motorlib +from motorlib.enums.unit import Unit from ..tool import Tool class NeutralBatesTool(Tool): def __init__(self, manager): - props = {'length': motorlib.properties.FloatProperty('Propellant length', 'm', 0, 10), - 'diameter': motorlib.properties.FloatProperty('Propellant diameter', 'm', 0, 1), - 'grainSpace': motorlib.properties.FloatProperty('Grain spacer length', 'm', 0, 1), + props = {'length': motorlib.properties.FloatProperty('Propellant length', Unit.METER, 0, 10), + 'diameter': motorlib.properties.FloatProperty('Propellant diameter', Unit.METER, 0, 1), + 'grainSpace': motorlib.properties.FloatProperty('Grain spacer length', Unit.METER, 0, 1), 'Kn': motorlib.properties.FloatProperty('Initial Kn', '', 1, 1000)} super().__init__(manager, diff --git a/uilib/widgets/burnrateGraph.py b/uilib/widgets/burnrateGraph.py index 5bafd71..decf0c0 100644 --- a/uilib/widgets/burnrateGraph.py +++ b/uilib/widgets/burnrateGraph.py @@ -1,6 +1,7 @@ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure +from motorlib.enums.unit import Unit from motorlib.units import convertAll class BurnrateGraph(FigureCanvas): @@ -23,15 +24,15 @@ def cleanup(self): self.draw() def showGraph(self, points): - presUnit = self.preferences.getUnit('Pa') - rateUnit = self.preferences.getUnit('m/s') + presUnit = self.preferences.getUnit(Unit.PASCAL) + rateUnit = self.preferences.getUnit(Unit.METER_PER_SECOND) # I really don't like this, but it is necessary for this graph and the c* output to coexist - if rateUnit == 'ft/s': - rateUnit = 'in/s' - if rateUnit == 'm/s': - rateUnit = 'mm/s' + if rateUnit == Unit.FOOT_PER_SECOND: + rateUnit = Unit.INCH_PER_SECOND + if rateUnit == Unit.METER_PER_SECOND: + rateUnit = Unit.MILLIMETER_PER_SECOND - self.plot.plot(convertAll(points[0], 'Pa', presUnit), convertAll(points[1], 'm/s', rateUnit)) + self.plot.plot(convertAll(points[0], Unit.PASCAL, presUnit), convertAll(points[1], Unit.METER_PER_SECOND, rateUnit)) self.plot.set_xlabel('Pressure - {}'.format(presUnit)) self.plot.set_ylabel('Burn Rate - {}'.format(rateUnit)) self.plot.grid(True) diff --git a/uilib/widgets/grainPreviewGraph.py b/uilib/widgets/grainPreviewGraph.py index 89eb6c0..e32b598 100644 --- a/uilib/widgets/grainPreviewGraph.py +++ b/uilib/widgets/grainPreviewGraph.py @@ -38,7 +38,7 @@ def cleanup(self): self.image = None if self.numContours > 0: for _ in range(0, self.numContours): - self.plot.lines.pop(0) + self.plot.lines[0].remove() self.numContours = 0 self.draw() diff --git a/uilib/widgets/grainPreviewWidget.py b/uilib/widgets/grainPreviewWidget.py index 2055c0d..de57388 100644 --- a/uilib/widgets/grainPreviewWidget.py +++ b/uilib/widgets/grainPreviewWidget.py @@ -3,7 +3,7 @@ from PyQt5.QtWidgets import QWidget from PyQt5.QtCore import pyqtSignal -import motorlib +from motorlib.enums.simAlertLevel import SimAlertLevel from ..views.GrainPreview_ui import Ui_GrainPreview @@ -30,7 +30,7 @@ def loadGrain(self, grain): self.ui.tabAlerts.addItem(err.description) for alert in geomAlerts: - if alert.level == motorlib.simResult.SimAlertLevel.ERROR: + if alert.level == SimAlertLevel.ERROR: return dataThread = Thread(target=self._genData, args=[grain]) diff --git a/uilib/widgets/mainWindow.py b/uilib/widgets/mainWindow.py index e2589ff..037e92e 100644 --- a/uilib/widgets/mainWindow.py +++ b/uilib/widgets/mainWindow.py @@ -1,13 +1,15 @@ import sys -from PyQt5.QtWidgets import QWidget, QMainWindow, QTableWidgetItem, QHeaderView, QTableWidget from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QMainWindow, QTableWidgetItem, QHeaderView import motorlib import uilib.widgets.aboutDialog import uilib.widgets.preferencesMenu +from motorlib.enums.unit import Unit from uilib.views.MainWindow_ui import Ui_MainWindow + class Window(QMainWindow): def __init__(self, app): QMainWindow.__init__(self) @@ -25,10 +27,14 @@ def __init__(self, app): self.app.propellantManager.updated.connect(self.propListChanged) - self.motorStatLabels = [self.ui.labelMotorDesignation, self.ui.labelImpulse, self.ui.labelDeliveredISP, self.ui.labelBurnTime, self.ui.labelVolumeLoading, - self.ui.labelAveragePressure, self.ui.labelPeakPressure, self.ui.labelInitialKN, self.ui.labelPeakKN, self.ui.labelIdealThrustCoefficient, - self.ui.labelPropellantMass, self.ui.labelPropellantLength, self.ui.labelPortThroatRatio, self.ui.labelPeakMassFlux, self.ui.labelDeliveredThrustCoefficient - ] + self.motorStatLabels = [self.ui.labelMotorDesignation, self.ui.labelImpulse, self.ui.labelDeliveredISP, + self.ui.labelBurnTime, self.ui.labelVolumeLoading, + self.ui.labelAveragePressure, self.ui.labelPeakPressure, self.ui.labelInitialKN, + self.ui.labelPeakKN, self.ui.labelIdealThrustCoefficient, + self.ui.labelPropellantMass, self.ui.labelPropellantLength, + self.ui.labelPortThroatRatio, self.ui.labelPeakMassFlux, + self.ui.labelDeliveredThrustCoefficient + ] self.app.fileManager.fileNameChanged.connect(self.updateWindowTitle) self.app.fileManager.newMotor.connect(self.resetOutput) @@ -144,7 +150,7 @@ def setupGrainTable(self): self.ui.tableWidgetGrainList.itemSelectionChanged.connect(self.checkGrainSelection) self.checkGrainSelection() - + self.ui.tableWidgetGrainList.doubleClicked.connect(self.doubleClickGrainSelector) def setupGraph(self): @@ -183,13 +189,14 @@ def propChooserChanged(self): def updateGrainTable(self): cm = self.app.fileManager.getCurrentMotor() self.ui.tableWidgetGrainList.setRowCount(len(cm.grains) + 2) - lengthUnit = self.app.preferencesManager.preferences.units.getProperty('m') + Unit = self.app.preferencesManager.preferences.units.getProperty('m') for gid, grain in enumerate(cm.grains): self.ui.tableWidgetGrainList.setItem(gid, 0, QTableWidgetItem(grain.geomName)) - self.ui.tableWidgetGrainList.setItem(gid, 1, QTableWidgetItem(grain.getDetailsString(lengthUnit))) + self.ui.tableWidgetGrainList.setItem(gid, 1, QTableWidgetItem(grain.getDetailsString(Unit))) self.ui.tableWidgetGrainList.setItem(len(cm.grains), 0, QTableWidgetItem('Nozzle')) - self.ui.tableWidgetGrainList.setItem(len(cm.grains), 1, QTableWidgetItem(cm.nozzle.getDetailsString(lengthUnit))) + self.ui.tableWidgetGrainList.setItem(len(cm.grains), 1, + QTableWidgetItem(cm.nozzle.getDetailsString(Unit))) self.ui.tableWidgetGrainList.setItem(len(cm.grains) + 1, 0, QTableWidgetItem('Config')) self.ui.tableWidgetGrainList.setItem(len(cm.grains) + 1, 1, QTableWidgetItem('-')) @@ -215,11 +222,11 @@ def checkGrainSelection(self): if len(ind) > 0: gid = ind[0].row() self.toggleGrainButtons(True) - if gid == 0: # Top grain selected + if gid == 0: # Top grain selected self.ui.pushButtonMoveGrainUp.setEnabled(False) - if gid == len(cm.grains) - 1: # Bottom grain selected + if gid == len(cm.grains) - 1: # Bottom grain selected self.ui.pushButtonMoveGrainDown.setEnabled(False) - if gid >= len(cm.grains): # Nozzle or config selected + if gid >= len(cm.grains): # Nozzle or config selected self.ui.pushButtonMoveGrainUp.setEnabled(False) self.ui.pushButtonMoveGrainDown.setEnabled(False) self.ui.pushButtonDeleteGrain.setEnabled(False) @@ -292,31 +299,33 @@ def formatMotorStat(self, quantity, inUnit): def updateMotorStats(self, simResult): self.ui.labelMotorDesignation.setText(simResult.getDesignation()) - self.ui.labelImpulse.setText(self.formatMotorStat(simResult.getImpulse(), 'Ns')) - self.ui.labelDeliveredISP.setText(self.formatMotorStat(simResult.getISP(), 's')) - self.ui.labelBurnTime.setText(self.formatMotorStat(simResult.getBurnTime(), 's')) + self.ui.labelImpulse.setText(self.formatMotorStat(simResult.getImpulse(), Unit.NEWTON_SECOND)) + self.ui.labelDeliveredISP.setText(self.formatMotorStat(simResult.getISP(), Unit.SECOND)) + self.ui.labelBurnTime.setText(self.formatMotorStat(simResult.getBurnTime(), Unit.SECOND)) self.ui.labelVolumeLoading.setText('{:.2f}%'.format(simResult.getVolumeLoading())) - self.ui.labelAveragePressure.setText(self.formatMotorStat(simResult.getAveragePressure(), 'Pa')) - self.ui.labelPeakPressure.setText(self.formatMotorStat(simResult.getMaxPressure(), 'Pa')) + self.ui.labelAveragePressure.setText(self.formatMotorStat(simResult.getAveragePressure(), Unit.PASCAL)) + self.ui.labelPeakPressure.setText(self.formatMotorStat(simResult.getMaxPressure(), Unit.PASCAL)) self.ui.labelInitialKN.setText(self.formatMotorStat(simResult.getInitialKN(), '')) self.ui.labelPeakKN.setText(self.formatMotorStat(simResult.getPeakKN(), '')) self.ui.labelIdealThrustCoefficient.setText(self.formatMotorStat(simResult.getIdealThrustCoefficient(), '')) - self.ui.labelPropellantMass.setText(self.formatMotorStat(simResult.getPropellantMass(), 'kg')) - self.ui.labelPropellantLength.setText(self.formatMotorStat(simResult.getPropellantLength(), 'm')) + self.ui.labelPropellantMass.setText(self.formatMotorStat(simResult.getPropellantMass(), Unit.KILOGRAM)) + self.ui.labelPropellantLength.setText(self.formatMotorStat(simResult.getPropellantLength(), Unit.METER)) # These only make sense for grains with cores, so blank them out for endburners if simResult.getPortRatio() is not None: self.ui.labelPortThroatRatio.setText(self.formatMotorStat(simResult.getPortRatio(), '')) - peakMassFluxQuantity = self.formatMotorStat(simResult.getPeakMassFlux(), 'kg/(m^2*s)') + peakMassFluxQuantity = self.formatMotorStat(simResult.getPeakMassFlux(), + Unit.KILOGRAM_PER_SQUARE_METER_PER_SECOND) peakMassFluxGrain = simResult.getPeakMassFluxLocation() + 1 self.ui.labelPeakMassFlux.setText('{} (G: {})'.format(peakMassFluxQuantity, peakMassFluxGrain)) else: self.ui.labelPortThroatRatio.setText('-') self.ui.labelPeakMassFlux.setText('-') - self.ui.labelDeliveredThrustCoefficient.setText(self.formatMotorStat(simResult.getAdjustedThrustCoefficient(), '')) + self.ui.labelDeliveredThrustCoefficient.setText( + self.formatMotorStat(simResult.getAdjustedThrustCoefficient(), '')) def runSimulation(self): self.resetOutput() @@ -361,7 +370,7 @@ def loadMotor(self, path=None): # Clear out all info related to old motor/sim in the interface def postLoadUpdate(self): - self.disablePropSelector() # It is enabled again at the end of updatePropBoxSelection + self.disablePropSelector() # It is enabled again at the end of updatePropBoxSelection self.resetOutput() self.updateGrainTable() self.populatePropSelector() diff --git a/uilib/widgets/nozzlePreviewWidget.py b/uilib/widgets/nozzlePreviewWidget.py index aec1496..4f8d81d 100644 --- a/uilib/widgets/nozzlePreviewWidget.py +++ b/uilib/widgets/nozzlePreviewWidget.py @@ -4,7 +4,7 @@ from PyQt5.QtGui import QPolygonF, QBrush from PyQt5.QtCore import QPointF, Qt -import motorlib +from motorlib.enums.simAlertLevel import SimAlertLevel from ..views.NozzlePreview_ui import Ui_NozzlePreview class NozzlePreviewWidget(QWidget): @@ -37,7 +37,7 @@ def loadNozzle(self, nozzle): self.lower.setPolygon(QPolygonF([])) for alert in geomAlerts: - if alert.level == motorlib.simResult.SimAlertLevel.ERROR: + if alert.level == SimAlertLevel.ERROR: return convAngle = radians(nozzle.props['convAngle'].getValue()) diff --git a/uilib/widgets/propellantPreviewWidget.py b/uilib/widgets/propellantPreviewWidget.py index b6a3420..e79b63e 100644 --- a/uilib/widgets/propellantPreviewWidget.py +++ b/uilib/widgets/propellantPreviewWidget.py @@ -1,10 +1,9 @@ from PyQt5.QtWidgets import QWidget -from PyQt5.QtCore import pyqtSignal - -import motorlib +from motorlib.enums.simAlertLevel import SimAlertLevel from ..views.PropellantPreview_ui import Ui_PropellantPreview + class PropellantPreviewWidget(QWidget): def __init__(self): super().__init__() @@ -22,11 +21,11 @@ def loadPropellant(self, propellant): self.ui.tabAlerts.addItem(err.description) for alert in alerts: - if alert.level == motorlib.simResult.SimAlertLevel.ERROR: + if alert.level == SimAlertLevel.ERROR: return burnrateData = [[], []] - minPres = int(propellant.getMinimumValidPressure()) + 1 # Add 1 Pa to avoid crashing on burnrate for 0 Pa + minPres = int(propellant.getMinimumValidPressure()) + 1 # Add 1 Pa to avoid crashing on burnrate for 0 Pa maxPres = int(propellant.getMaximumValidPressure()) for pres in range(minPres, maxPres, 2000): burnrateData[0].append(pres) diff --git a/uilib/widgets/propellantTabEditor.py b/uilib/widgets/propellantTabEditor.py index b93846b..db7724b 100644 --- a/uilib/widgets/propellantTabEditor.py +++ b/uilib/widgets/propellantTabEditor.py @@ -1,6 +1,7 @@ from PyQt5.QtWidgets import QLabel from PyQt5.QtCore import pyqtSignal +from motorlib.enums.unit import Unit from motorlib.units import convert from motorlib.propellant import PropellantTab from motorlib.constants import gasConstant @@ -27,11 +28,11 @@ def propertyUpdate(self): charVel = num / denom if self.preferences is not None: - dispUnit = self.preferences.getUnit('m/s') + dispUnit = self.preferences.getUnit(Unit.METER_PER_SECOND) else: - dispUnit = 'm/s' + dispUnit = Unit.METER_PER_SECOND - cStarText = '{} {}'.format(int(convert(charVel, 'm/s', dispUnit)), dispUnit) + cStarText = '{} {}'.format(int(convert(charVel, Unit.METER_PER_SECOND, dispUnit)), dispUnit) self.labelCStar.setText('Characteristic Velocity: {}'.format(cStarText)) self.modified.emit() @@ -43,15 +44,15 @@ def cleanup(self): def getProperties(self): # Override to change units on ballistic coefficient res = super().getProperties() coeffUnit = self.propertyEditors['a'].dispUnit - if coeffUnit == 'in/(s*psi^n)': + if coeffUnit == Unit.INCH_PER_SECOND_POUND_PER_SQUARE_INCH_TO_THE_POWER_OF_N: res['a'] *= 1 / (6895 ** res['n']) return res def loadProperties(self, obj): # Override for ballistic coefficient units props = obj.getProperties() # Convert the ballistic coefficient based on the exponent - ballisticCoeffUnit = self.preferences.getUnit('m/(s*Pa^n)') - if ballisticCoeffUnit == 'in/(s*psi^n)': + ballisticCoeffUnit = self.preferences.getUnit(Unit.METER_PER_SECOND_PASCAL_TO_THE_POWER_OF_N) + if ballisticCoeffUnit == Unit.INCH_PER_SECOND_POUND_PER_SQUARE_INCH_TO_THE_POWER_OF_N: props['a'] /= 1 / (6895 ** props['n']) # Create a new propellant instance using the new A newPropTab = PropellantTab() diff --git a/uilib/widgets/resultsWidget.py b/uilib/widgets/resultsWidget.py index b23aaa3..89a21d8 100644 --- a/uilib/widgets/resultsWidget.py +++ b/uilib/widgets/resultsWidget.py @@ -2,7 +2,9 @@ import numpy as np import motorlib -from motorlib.simResult import singleValueChannels, multiValueChannels +from motorlib.enums.multiValueChannels import MultiValueChannels +from motorlib.enums.singleValueChannels import SingleValueChannels +from motorlib.enums.unit import Unit from .grainImageWidget import GrainImageWidget @@ -11,7 +13,10 @@ class ResultsWidget(QWidget): # These channels are extracted from the simResult amd put into the grain table in this order that should match # the labels in the .ui file - grainTableFields = ('mass', 'massFlow', 'massFlux', 'web') + grainTableFields = (MultiValueChannels.MASS, + MultiValueChannels.MASS_FLOW, + MultiValueChannels.MASS_FLUX, + MultiValueChannels.WEB) def __init__(self, parent): super().__init__(parent) @@ -21,10 +26,22 @@ def __init__(self, parent): self.simResult = None self.cachedChecks = None - excludes = ['kn', 'pressure', 'force', 'mass', 'massFlow', 'massFlux', 'exitPressure', 'dThroat', 'volumeLoading'] - self.ui.channelSelectorX.setupChecks(False, default='time', exclude=excludes) + excludes = [SingleValueChannels.KN, + SingleValueChannels.PRESSURE, + SingleValueChannels.FORCE, + MultiValueChannels.MASS, + MultiValueChannels.MASS_FLOW, + MultiValueChannels.MASS_FLUX, + SingleValueChannels.EXIT_PRESSURE, + SingleValueChannels.D_THROAT, + SingleValueChannels.VOLUME_LOADING] + self.ui.channelSelectorX.setupChecks(False, default=SingleValueChannels.TIME, exclude=excludes) self.ui.channelSelectorX.setTitle('X Axis') - self.ui.channelSelectorY.setupChecks(True, default=['kn', 'pressure', 'force'], exclude=['time']) + self.ui.channelSelectorY.setupChecks(True, + default=[SingleValueChannels.KN, + SingleValueChannels.PRESSURE, + SingleValueChannels.FORCE], + exclude=[SingleValueChannels.TIME]) self.ui.channelSelectorY.setTitle('Y Axis') self.ui.channelSelectorX.checksChanged.connect(self.xSelectionChanged) self.ui.channelSelectorY.checksChanged.connect(self.drawGraphs) @@ -53,7 +70,7 @@ def showData(self, simResult): self.drawGraphs() self.cleanupGrainTab() - self.ui.horizontalSliderTime.setMaximum(len(simResult.channels['time'].getData()) - 1) + self.ui.horizontalSliderTime.setMaximum(len(simResult.channels[SingleValueChannels.TIME].getData()) - 1) self.ui.tableWidgetGrains.setColumnCount(len(simResult.motor.grains)) for _ in range(len(self.grainImageWidgets)): del self.grainImageWidgets[-1] @@ -71,11 +88,11 @@ def showData(self, simResult): self.updateGrainTab() def xSelectionChanged(self): - if self.ui.channelSelectorX.getSelectedChannels()[0] in multiValueChannels: - self.ui.channelSelectorY.unselect(singleValueChannels) - self.ui.channelSelectorY.toggleEnabled(singleValueChannels, False) + if self.ui.channelSelectorX.getSelectedChannels()[0] in MultiValueChannels: + self.ui.channelSelectorY.unselect(SingleValueChannels) + self.ui.channelSelectorY.toggleEnabled(SingleValueChannels, False) else: - self.ui.channelSelectorY.toggleEnabled(singleValueChannels, True) + self.ui.channelSelectorY.toggleEnabled(SingleValueChannels, True) self.drawGraphs() def drawGraphs(self): @@ -91,8 +108,8 @@ def updateGrainTab(self): index = self.ui.horizontalSliderTime.value() for gid, grain in enumerate(self.simResult.motor.grains): if self.grainImages[gid] is not None: - regDist = self.simResult.channels['regression'].getPoint(index)[gid] - webRemaining = self.simResult.channels['web'].getPoint(index)[gid] + regDist = self.simResult.channels[MultiValueChannels.REGRESSION].getPoint(index)[gid] + webRemaining = self.simResult.channels[MultiValueChannels.WEB].getPoint(index)[gid] hasWebLeft = webRemaining > self.simResult.motor.config.getProperty('burnoutWebThres') mapDist = regDist / (0.5 * grain.props['diameter'].getValue()) image = np.logical_and(self.grainImages[gid] > mapDist, hasWebLeft) @@ -106,22 +123,22 @@ def updateGrainTab(self): val = motorlib.units.convert(self.simResult.channels[field].getPoint(index)[gid], fromUnit, toUnit) self.grainLabels[gid][field].setText('{:.3f} {}'.format(val, toUnit)) - currentTime = self.simResult.channels['time'].getPoint(index) - remainingTime = self.simResult.channels['time'].getLast() - currentTime + currentTime = self.simResult.channels[SingleValueChannels.TIME].getPoint(index) + remainingTime = self.simResult.channels[SingleValueChannels.TIME].getLast() - currentTime self.ui.labelTimeProgress.setText('{:.3f} s'.format(currentTime)) self.ui.labelTimeRemaining.setText('{:.3f} s'.format(remainingTime)) currentImpulse = self.simResult.getImpulse(index) remainingImpulse = self.simResult.getImpulse() - currentImpulse - impUnit = self.preferences.getUnit('Ns') - self.ui.labelImpulseProgress.setText(motorlib.units.convFormat(currentImpulse, 'Ns', impUnit)) - self.ui.labelImpulseRemaining.setText(motorlib.units.convFormat(remainingImpulse, 'Ns', impUnit)) + impUnit = self.preferences.getUnit(Unit.NEWTON_SECOND) + self.ui.labelImpulseProgress.setText(motorlib.units.convFormat(currentImpulse, Unit.NEWTON_SECOND, impUnit)) + self.ui.labelImpulseRemaining.setText(motorlib.units.convFormat(remainingImpulse, Unit.NEWTON_SECOND, impUnit)) currentMass = self.simResult.getPropellantMass(index) remainingMass = self.simResult.getPropellantMass() - currentMass - massUnit = self.preferences.getUnit('kg') - self.ui.labelMassProgress.setText(motorlib.units.convFormat(remainingMass, 'kg', massUnit)) - self.ui.labelMassRemaining.setText(motorlib.units.convFormat(currentMass, 'kg', massUnit)) + massUnit = self.preferences.getUnit(Unit.KILOGRAM) + self.ui.labelMassProgress.setText(motorlib.units.convFormat(remainingMass, Unit.KILOGRAM, massUnit)) + self.ui.labelMassRemaining.setText(motorlib.units.convFormat(currentMass, Unit.KILOGRAM, massUnit)) currentISP = self.simResult.getISP(index) self.ui.labelISPProgress.setText('{:.3f} s'.format(currentISP)) diff --git a/uilib/widgets/simulationAlertsDialog.py b/uilib/widgets/simulationAlertsDialog.py index 880d1cb..a4974d3 100644 --- a/uilib/widgets/simulationAlertsDialog.py +++ b/uilib/widgets/simulationAlertsDialog.py @@ -1,7 +1,5 @@ from PyQt5.QtWidgets import QDialog, QTableWidgetItem, QHeaderView, QApplication -from motorlib.simResult import alertLevelNames, alertTypeNames - from ..views.SimulationAlertsDialog_ui import Ui_SimAlertsDialog class SimulationAlertsDialog(QDialog): @@ -27,8 +25,8 @@ def displayAlerts(self, simRes): self.ui.tableWidgetAlerts.setRowCount(len(simRes.alerts)) for row, alert in enumerate(simRes.alerts): - self.ui.tableWidgetAlerts.setItem(row, 0, QTableWidgetItem(alertLevelNames[alert.level])) - self.ui.tableWidgetAlerts.setItem(row, 1, QTableWidgetItem(alertTypeNames[alert.type])) + self.ui.tableWidgetAlerts.setItem(row, 0, QTableWidgetItem(alert.level)) + self.ui.tableWidgetAlerts.setItem(row, 1, QTableWidgetItem(alert.type)) self.ui.tableWidgetAlerts.setItem(row, 2, QTableWidgetItem(alert.location)) self.ui.tableWidgetAlerts.setItem(row, 3, QTableWidgetItem(alert.description)) self.show()