How do I use it?

This example will walk you through the basic process of building a model with the model-framework, and also demonstrate how to provide a simple, and documented user interface.

What we want to achieve?

Let’s start at the end. What do we actually want to achieve here? We want to build a simple Python user interface for a model. As an example, let’s consider a very simple aircraft model. We will implement the so-called Breguet range equation which can be used to estimate the range of an aircraft. According to the Breguet equitation, the range for a jet aircraft in cruise conditions can be estimated from the following relation.

\[R = \frac{a \, M}{g \, c_\textrm{T}} \, \frac{C_\textrm{L}}{C_\textrm{D}} \, \textrm{ln} \, \frac{m_1}{m_2}\]
  • \(R\): range [m]

  • \(a\): speed of sound [m/s]

  • \(M\): Mach number [1]

  • \(c_\textrm{T}\): thrust specific fuel consumption [kg/(s*N)]

  • \(C_\textrm{L}\): lift coefficient [1]

  • \(C_\textrm{D}\): drag coefficient [1]

  • \(m_1\): initial mass [kg]

  • \(m_2\): final mass [kg]

The exact derivation and limitation of this formula are not relevant for the following discussion. What we want, is to provide a Python API which picks up and validates the user input. The user API can be used in the following way:

from aircraft import Model

# First, we create a new aircraft model instance
ac = Model()

# In our case the aircraft model, will have the feature 'aerodynamics'.
# Below, we first create an instance of this feature, and subsequently
# we assign numerical values to the aerodynamic properties.
aero = ac.set_feature('aerodynamics')
aero.set('CL', 1.5)
aero.set('CD', 0.08)
aero.set('Mach', 0.8)

# Our aircraft model also has a 'ambiance' feature to set the gravitational
# acceleration and speed of sound.
amb = ac.set_feature('ambiance')
amb.set('g', 9.8)
amb.set('a', 300.0)

# We can set the thrust specific fuel consumption in the feature 'propulsion'
prop = ac.set_feature('propulsion')
prop.set('cT', 20e-6)

# Finally, we have a feature called 'mass' where we assign initial and final
# masses
mass = ac.set_feature('mass')
mass.set('m1', 70e3)

# Once, we have set up the model we can call the 'run()' method which
# will compute the range for both configurations and print the results.
for m2 in range(35, 61, 5):
    mass.set('m2', m2*1e3)
    ac.run()

When running this example, we get rages for the chosen combinations of initial and final masses.

>>> import example
Range: 15914.1 km (m1: 70.0 t | m2: 35.0 t)
Range: 12848.3 km (m1: 70.0 t | m2: 40.0 t)
Range: 10144.1 km (m1: 70.0 t | m2: 45.0 t)
Range:  7725.1 km (m1: 70.0 t | m2: 50.0 t)
Range:  5536.9 km (m1: 70.0 t | m2: 55.0 t)
Range:  3539.2 km (m1: 70.0 t | m2: 60.0 t)
>>>

Of course, the model discussed here is very simple, and it would not make sense to provide such an elaborate and explicit API to solve the simple range formula. However, for complex models it may become much harder to provide a user interface which is simple to use and extensible, and it may become hard to document all available user inputs. The model-framework addresses these issues.

How do we build the API?

Python module file structure

We will assume the following file structure for our example:

aircraft/
    __init__.py
    _model.py
    _run.py

Our goal is to provide the Model object that we used above and that provides all user methods. Why we organize our module Python files in the structure as suggested above, will hopefully become clearer as we go along in this tutorial.

Building the user class

Let’s start at the end: we want the user to import our model with from aircraft import Model. To achive this, we provide a reference to the Model object in the __init__.py.

# __index__.py
from ._model import Model

Okay, but so far the file _model.py is empty, and does not contain the object Model. The code below shows how.

# _model.py
from mframework import FeatureSpec, ModelSpec

from ._run import run_model

# Here, we only have numerical user input. We only allow positive floats.
SCHEMA_POS_FLOAT = {'type': float, '>': 0}

# ===== MODEL =====
mspec = ModelSpec()

# Create the first feature 'ambiance'
fspec = FeatureSpec()
fspec.add_prop_spec('g', SCHEMA_POS_FLOAT, doc='Gravitational acceleration', max_items=1)
fspec.add_prop_spec('a', SCHEMA_POS_FLOAT, doc='Speed of sound', max_items=1)
mspec.add_feature_spec('ambiance', fspec, doc='Ambient flight conditions', max_items=1)

# Feature 'aerodynamics'
fspec = FeatureSpec()
fspec.add_prop_spec('CL', SCHEMA_POS_FLOAT, doc='Cruise lift coefficient', max_items=1)
fspec.add_prop_spec('CD', SCHEMA_POS_FLOAT, doc='Cruise drag coefficient', max_items=1)
fspec.add_prop_spec('Mach', SCHEMA_POS_FLOAT, doc='Cruise Mach number', max_items=1)
mspec.add_feature_spec('aerodynamics', fspec, doc='Aerodynamic properties', max_items=1)

# Feature 'propulsion'
fspec = FeatureSpec()
fspec.add_prop_spec('cT', SCHEMA_POS_FLOAT, doc='Thrust specific fuel consumption', max_items=1)
mspec.add_feature_spec('propulsion', fspec, doc='Profusion properties', max_items=1)

# Feature 'mass'
fspec = FeatureSpec()
fspec.add_prop_spec('m1', SCHEMA_POS_FLOAT, doc='Initial aircraft mass (at start of cruise)', max_items=1)
fspec.add_prop_spec('m2', SCHEMA_POS_FLOAT, doc='Final aircraft mass (at end of cruise)', max_items=1)
mspec.add_feature_spec('mass', fspec, doc='Mass properties', max_items=1)

# ===== RESULT =====
rspec = ModelSpec()

# Feature 'flight_mission'
fspec = FeatureSpec()
fspec.add_prop_spec('range', SCHEMA_POS_FLOAT, doc='Estimated range of the aircraft', max_items=1)
rspec.add_feature_spec('flight_mission', fspec, doc='Flight mission data', max_items=1)

mspec.results = rspec


class Model(mspec.user_class):
    def run(self):
        super().run()
        run_model(self)

When defining the method run() in Model, we passed the model instance to another method called run_model. We define this method in the file _run.py.

# _run.py
from math import log


def run_model(model):
    """Run the full model analysis"""

    # Compute the aircraft range
    breguet_range(model)

    m1 = model.get('mass').get('m1')
    m2 = model.get('mass').get('m2')
    r = model.results.get('flight_mission').get('range')
    print(f"Range: {r/1000:7.1f} km (m1: {m1/1e3:.1f} t | m2: {m2/1e3:.1f} t)")


def breguet_range(model):
    """Estimate the range"""

    M = model.get('aerodynamics').get('Mach')
    cD = model.get('aerodynamics').get('CD')
    cL = model.get('aerodynamics').get('CL')

    a = model.get('ambiance').get('a')
    g = model.get('ambiance').get('g')

    cT = model.get('propulsion').get('cT')

    m1 = model.get('mass').get('m1')
    m2 = model.get('mass').get('m2')

    # Solve Breguet equation
    r = a*M*cL*log(m1/m2)/(g*cT*cD)

    fm = model.results.set_feature('flight_mission')
    fm.set('range', r)

That’s it. In this simplistic example, the procedure may rightfully seem very elaborate and perhaps overly complicate. However, for complex models this process pay off, not at least for the user documentation which can be generated fully automatically.

How do we build user documentation?

Below, you will see the actual script which is used to generate the documentation in this tutorial.

import os
from pathlib import Path

from mframework import doc2rst

from aircraft._model import mspec

HERE = os.path.abspath(os.path.dirname(__file__))
doc_path = os.path.join(HERE, 'autodoc')

Path(doc_path).mkdir(parents=True, exist_ok=True)
doc2rst(mspec, doc_path)

After running this script, we get the following pages:

Further options and references

API documentation

For a more comprehensive overview about the functionality, please check out the API documentation for model-framework.

schemadict

More details about schemadict can be found here: https://github.com/airinnova/schemadict.