diff --git a/motorlib/grains/finocyl.py b/motorlib/grains/finocyl.py index f82c53d..e087e18 100644 --- a/motorlib/grains/finocyl.py +++ b/motorlib/grains/finocyl.py @@ -3,7 +3,7 @@ import numpy as np from ..grain import FmmGrain -from ..properties import FloatProperty, IntProperty +from ..properties import FloatProperty, IntProperty, BooleanProperty from ..simResult import SimAlert, SimAlertLevel, SimAlertType class Finocyl(FmmGrain): @@ -16,13 +16,17 @@ def __init__(self): 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['invertedFins'] = BooleanProperty('Inverted fins') def generateCoreMap(self): coreRadius = self.normalize(self.props['coreDiameter'].getValue()) / 2 numFins = self.props['numFins'].getValue() finWidth = self.normalize(self.props['finWidth'].getValue()) - # The user enters the length that the fin protrudes from the core, so we add the radius on - finLength = self.normalize(self.props['finLength'].getValue()) + coreRadius + finLength = self.normalize(self.props['finLength'].getValue()) + invertedFins = self.props['invertedFins'].getValue() + + finStart = coreRadius - finLength if invertedFins else 0 + finEnd = coreRadius if invertedFins else finLength + coreRadius # Open up core self.coreMap[self.mapX**2 + self.mapY**2 < coreRadius**2] = 0 @@ -36,11 +40,11 @@ def generateCoreMap(self): # Select all points within half the width of the vector vect = abs(vect0*self.mapX + vect1*self.mapY) < finWidth / 2 # Set up two perpendicular vectors to cap off the ends - near = (vect1 * self.mapX) - (vect0 * self.mapY) > 0 # Inside of the core - far = (vect1 * self.mapX) - (vect0 * self.mapY) < finLength # At the casting tube end of the vector + near = (vect1 * self.mapX) - (vect0 * self.mapY) > finStart + far = (vect1 * self.mapX) - (vect0 * self.mapY) < finEnd ends = np.logical_and(far, near) - # Open up the fin - self.coreMap[np.logical_and(vect, ends)] = 0 + # For inverted fins, we are filling propellant back in. For regular fins, we are removing it. + self.coreMap[np.logical_and(vect, ends)] = invertedFins def getDetailsString(self, lengthUnit='m'): return 'Length: {}, Core: {}, Fins: {}'.format(self.props['length'].dispFormat(lengthUnit), @@ -60,18 +64,34 @@ def getGeometryErrors(self): if self.props['finLength'].getValue() * 2 > self.props['diameter'].getValue(): aText = 'Fin length should be less than or equal to grain radius' errors.append(SimAlert(SimAlertLevel.WARNING, SimAlertType.GEOMETRY, aText)) - coreWidth = self.props['coreDiameter'].getValue() + (2 * self.props['finLength'].getValue()) - if coreWidth > self.props['diameter'].getValue(): - aText = 'Core radius plus fin length should be less than or equal to grain radius' - errors.append(SimAlert(SimAlertLevel.WARNING, SimAlertType.GEOMETRY, aText)) + + if self.props['invertedFins'].getValue(): + coreRadius = self.props['coreDiameter'].getValue() / 2 + # In the weird case of one fin that extends beyond the core, we need to make sure it doesn't + # intersect the core again on the other side as that would divide the port + if self.props['finLength'].getValue() > coreRadius: + lengthPastCenter = self.props['finLength'].getValue() - coreRadius + halfWidth = self.props['finWidth'].getValue() / 2 + tipRadius = (lengthPastCenter ** 2 + halfWidth ** 2) ** 0.5 + if tipRadius > coreRadius and self.props['numFins'].getValue() > 0: + aText = 'Fin tips outside of core' + errors.append(SimAlert(SimAlertLevel.ERROR, SimAlertType.GEOMETRY, aText)) + else: + coreWidth = self.props['coreDiameter'].getValue() + (2 * self.props['finLength'].getValue()) + if coreWidth > self.props['diameter'].getValue(): + aText = 'Core radius plus fin length should be less than or equal to grain radius' + errors.append(SimAlert(SimAlertLevel.WARNING, SimAlertType.GEOMETRY, aText)) if self.props['finWidth'].getValue() == 0: errors.append(SimAlert(SimAlertLevel.ERROR, SimAlertType.GEOMETRY, 'Fin width must not be 0')) if self.props['numFins'].getValue() > 1: radius = self.props['coreDiameter'].getValue() / 2 - apothem = radius + self.props['finLength'].getValue() + finLength = self.props['finLength'].getValue() + invertedFins = self.props['invertedFins'].getValue() + level = SimAlertLevel.ERROR if invertedFins else SimAlertLevel.WARNING + apothem = radius - finLength if invertedFins else radius + finLength sideLength = 2 * apothem * np.tan(np.pi / self.props['numFins'].getValue()) if sideLength < self.props['finWidth'].getValue(): - errors.append(SimAlert(SimAlertLevel.WARNING, SimAlertType.GEOMETRY, 'Fin tips intersect')) + errors.append(SimAlert(level, SimAlertType.GEOMETRY, 'Fin tips intersect')) return errors diff --git a/motorlib/properties.py b/motorlib/properties.py index deae744..de50960 100644 --- a/motorlib/properties.py +++ b/motorlib/properties.py @@ -78,6 +78,12 @@ def __init__(self, dispName): super().__init__(dispName, '', str) +class BooleanProperty(Property): + """A property with a single boolean as the value""" + def __init__(self, dispName): + super().__init__(dispName, '', bool) + + class PolygonProperty(Property): """A property that contains a list of polygons, each a list of points""" def __init__(self, dispName): diff --git a/uilib/fileIO.py b/uilib/fileIO.py index f33a334..6d8253b 100644 --- a/uilib/fileIO.py +++ b/uilib/fileIO.py @@ -72,6 +72,9 @@ def passthrough(data): def migrateMotor_0_5_0_to_0_6_0(data): data['config']['sepPressureRatio'] = DEFAULT_PREFERENCES['general']['sepPressureRatio'] data['config']['flowSeparationWarnPercent'] = DEFAULT_PREFERENCES['general']['flowSeparationWarnPercent'] + for grain in data['grains']: + if grain['type'] == 'Finocyl': + grain['properties']['invertedFins'] = False return data # 0.4.0 to 0.5.0 diff --git a/uilib/widgets/propertyEditor.py b/uilib/widgets/propertyEditor.py index 1b27868..8d89210 100644 --- a/uilib/widgets/propertyEditor.py +++ b/uilib/widgets/propertyEditor.py @@ -1,8 +1,8 @@ import math -from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLineEdit +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QCheckBox from PyQt6.QtWidgets import QDoubleSpinBox, QSpinBox, QComboBox -from PyQt6.QtCore import pyqtSignal +from PyQt6.QtCore import pyqtSignal, Qt import motorlib @@ -56,6 +56,12 @@ def __init__(self, parent, prop, preferences): self.editor.setText(self.prop.getValue()) self.layout().addWidget(self.editor) + elif isinstance(prop, motorlib.properties.BooleanProperty): + self.editor = QCheckBox() + self.editor.setCheckState(Qt.CheckState.Checked if self.prop.getValue() else Qt.CheckState.Unchecked) + self.editor.stateChanged.connect(self.valueChanged.emit) + self.layout().addWidget(self.editor) + elif isinstance(prop, motorlib.properties.EnumProperty): self.editor = QComboBox() @@ -94,6 +100,9 @@ def getValue(self): if isinstance(self.prop, motorlib.properties.StringProperty): return self.editor.text() + if isinstance(self.prop, motorlib.properties.BooleanProperty): + return self.editor.isChecked() + if isinstance(self.prop, motorlib.properties.EnumProperty): return self.editor.currentText()