###################################### BladeDesinger One-On-One: Fundamentals ###################################### This tutorial is intended for developers who wish to implement their own specific turbomachine components. For a Users Guide read :ref:`v2-users-index`. ********** Fist steps ********** This step by step tutorial aims to address the question: How do I implement a an analytical camber line? This question is answered with an examplary simplified NACA 6 series camber line. This chapter is divided into four steps: #. :ref:`base-class-label` #. :ref:`init-label` #. :ref:`properties-label` #. :ref:`demanded-methods-label` The equation for the simplified 6-series camber line is the following: .. math:: f := \left\{ \begin{array}{l} [0, 1] \rightarrow \mathbb{R} \cup \{-\infty, +\infty\}\\ x \mapsto -\frac{C_{L0}}{4\pi}[(1-x)\ln|1-x|+x\ln|x|]\\ \end{array} \right. To build an arbitrary turbomachine component you have the perform these four steps. In the second tutorial we will show you how to build a custom profile. .. _base-class-label: Choosing the right base class ----------------------------- The BladeDesigner provides a base class for every component. This tutorial covers how to implement a simplified naca 6-series camber line, so we choose an AnalyticalCamberLine class as base class (line 3). It's located in the bladedesigner.baseclasses package (line 1). The AnalyticalCamberLine and its base classes provides some basic functionality like: - attribute checking - NURBS interpolation (not implemented yet) - calculating the angle of inflow - plotting - etc. For more information about the functionality of the AnalyticalCamberLine, CamberLine and Component class see here, here and here. .. code-block:: python :emphasize-lines: 1,3,4 :linenos: import bladedesigner.baseclasses as bcls class N6SCamberLine(bcls.AnalyticalCamberLine): pass .. _init-label: Initialization of attributes ---------------------------- The simplified naca 6-series camber has one parameter the lift coefficient. But first things first. We have to initialize the base classes (line 7) because the child class overwrites the inherited ``__init__`` method. For that purpose ``super`` accesses all parent classes and initializes their attributes via ``.__init__()``. In the previous section we mentioned that the base class provides a method which checks if the user attributes are initialized. We want to check the new user attribute self._lift_coefficient as well. To do this two steps are necessary: - Assign to self._lift_coefficient an Uninitialized instance (line 8). Uninitialized is a class without logic. We use them to categorize initialized and uninitialized attributes. If you call an Uninitialized instance it returns the name of the attribute. Uninitialized is located in the bladedesigner.foundation package (line 2). - Append 'lift_coefficient' to self._properties (line 9). All attributes contained in self._properties are categorized in initialized or uninitialized attributes. The convention is to append the attribute name without leading underscore. The idea behind the missing underscore is: We want to refer to a property and not a variable. More about properties in :ref:`properties-label`. .. code-block:: python :emphasize-lines: 2,6,7,8,9 :linenos: import bladedesigner.baseclasses as bcls import bladedesigner.foundation as fdn class N6SCamberLine(bcls.AnalyticalCamberLine): def __init__(self): super(N6SCamberLine, self).__init__() self._lift_coefficient = fdn.Uninitialized('lift_coefficient') self._properties.append('lift_coefficient') .. _properties-label: Implementation of properties ---------------------------- If an attribute changes the state of it's instance, the attribute must have a getter and a setter method. Why is that? Because we have to do some clean up if the state changes. We are hiding the getter and setter method with a decorator named property [#f1]_. The python documentation recommends properties instead of the direct use of getter and setter methods. The advantage is that attributes with and without getter and setter methods look the same. The naming convention for properties and attributes are: - the attribute name starts always with an underscore plus a proper name, e.g. ``_attribute_name`` - the property name is the attribute name without leading underscore, e.g. ``attribute_name`` Back to our camber line example. In :ref:`init-label` we identified the attribute lift coefficient. It's a state changing attribute (as opposed to an attribute which does not change the state of the turbomachine, such as a name of an attribute), hence we need a property. In agreement with our naming convention we named the lift coefficient attribute ``_lift_coefficient`` and the property ``lift_coefficient`` (line 16, 17, 20 and 22) The only task of the getter method (line 16 and 17) is to return the value of ``_lift_coefficient`` (line 18). The setter method (line 20 and 22) has three main tasks: - checking if the current and new value are the same (line 23) If so the hole body of the if statement will be skipped. - applying the the new value to ``_lift_coefficient`` (line 24) text - doing some cleaning work (line 25) text .. code-block:: python :emphasize-lines: 1,6,16,17,18,19,20,21,22,23,24,25 :linenos: import bladedesigner as bd import bladedesigner.baseclasses as bcls import bladedesigner.foundation as fdn rigor = bd.config.get('supervisor', 'rigor') class N6SCamberLine(bcls.AnalyticalCamberLine): def __init__(self): super(N6SCamberLine, self).__init__() self._lift_coefficient = fdn.Uninitialized('lift_coefficient') self._properties.append('lift_coefficient') @property def lift_coefficient(self): return self._lift_coefficient @lift_coefficient.setter @fdn.requires(lift_coefficient=(int, float), rigor=rigor) def lift_coefficient(self, lift_coefficient): if self._lift_coefficient != lift_coefficient: self._lift_coefficient = lift_coefficient self.update() .. rubric:: Footnotes .. [#f1] If you're not familiar with python decorators have a look in `PEP 318 `_. For more information about the property decorater click `here `_. .. _demanded-methods-label: demanded methods ---------------- .. code-block:: python :emphasize-lines: 27,28,29,30,31,32,33,35,36,37,38,39,40,41,42,43,44 :linenos: import bladedesigner as bd import bladedesigner.baseclasses as bcls import bladedesigner.foundation as fdn rigor = bd.config.get('supervisor', 'rigor') class N6SCamberLine(bcls.AnalyticalCamberLine): def __init__(self): super(N6SCamberLine, self).__init__() self._lift_coefficient = fdn.Uninitialized('lift_coefficient') self._properties.append('lift_coefficient') @property def lift_coefficient(self): return self._lift_coefficient @lift_coefficient.setter @fdn.requires(lift_coefficient=(int, float), rigor=rigor) def lift_coefficient(self, lift_coefficient): if self._lift_coefficient != lift_coefficient: self._lift_coefficient = lift_coefficient self.update() @fdn.cached_property def slopes(self): self._check_initialization() self._cached = True x = self.distribution(self.sample_rate) c_l = self.lift_coefficient return -c_l / 4. / np.pi * (np.log(x) - np.log(1 - x)) @fdn.memoized def as_array(self): self._check_initialization() self._cached = True x = self.distribution(self.sample_rate) c_l = self.lift_coefficient y = -c_l / 4. / np.pi * ((1 - x) * np.log(1 - x) + x * np.log(x)) y[0] = y[-1] = 0 return np.reshape(np.append(x, y), (-1, 2), "F")