Properties

Simulation programs need to manage a large amount of state information. With especially large programs, the data management task can cause problems:

  • Contributors find it harder and harder to master the number of interfaces necessary to make any useful additions to the program, so contributions slow down.

  • Runtime configurability becomes increasingly difficult, with different modules using different mechanisms (environment variables, custom specification files, command-line options, etc.).

  • The order of initialization of modules is complicated and brittle, since one module’s initialization routines might need to set or retrieve state information from an uninitialized module.

  • Extensibility through add-on scripts, specification files, etc. is limited to the state information that the program provides, and non-code-writing developers often have to wait too long for the developers to get around to adding a new variable.

The Property Manager system provides a single interface for chosen program state information, and allows the creation of new, user-specified variables dynamically at run-time. The latter capability is especially important for the JSBSim control system model because the various control system components (PID controllers, switches, summer, gains, etc.) that make up the control law definition for an aircraft exist only in a configuration file. At runtime, after parsing the component definitions, the components are instantiated, and the property manager creates a property to store the output value of each component.

Properties themselves are like global variables with selectively limited visibility (read or read/write) that are categorized into a hierarchical, tree-like structure that is similar to the structure of a Unix file system. The structure of the property tree includes a root node, sub nodes (like subdirectories) and end-nodes (properties). Similar to a Unix file system, properties can be referenced relative to the current node, or to the root node. Nodes can be grafted onto other nodes similar to symbolically linking files or directories to other files or directories in a file system. Properties are used throughout JSBSim and FlightGear to refer to specific parameters in program code. Properties can be assigned from the command line, from specification files and scripts, and even via a socket interface. Property names look like: position/h-sl-ft and aero/qbar-psf.

To illustrate the power of using properties and configuration files, consider the case of a high-performance jet aircraft model. Assume for a moment that a new switch has been added to the control panel for the example aircraft that allows the pilot to override pitch limits in the FCS. For FlightGear, the instrument panel is defined in a configuration file, and the switch is defined there for visual display. A property name is also assigned to the switch definition. Within the flight control portion of the JSBSim aircraft specification file, that same property name assigned to the pitch override switch in the instrument panel definition file can be used to channel the control laws through the desired path as a function of the switch position. No code needs to be touched.

Specific simulation parameters are available both from within JSBSim and in configuration file specifications via properties. As mentioned earlier, “properties” are the term we use to describe parameters that we can access or set from within a configuration file, or on the command line.

Many properties are standard properties, i.e. those properties that are always present for all vehicles. The aerodynamic coefficients, engines, thrusters, and flight control/autopilot models will also have dynamically defined properties. This is because the whole set of aerodynamic coefficients, engines, etc. will not be known until after the relevant configuration file for an aircraft is read. One must know the convention used to name the properties for these parameters in order to access them. As an example, the flight control system for the X-15 model features the following components, among others:

<flight_control name="X-15">
        <channel name="Pitch">
                <summer name="fcs/pitch-trim-sum">
                        <input> fcs/elevator-cmd-norm </input>
                        <input> fcs/pitch-trim-cmd-norm </input>
                        <clipto>
                                <min> -1 </min>
                                <max>  1 </max>
                        </clipto>
                </summer>
                <aerosurface_scale name="fcs/pitch-command-scale">
                        <input> fcs/pitch-trim-sum </input>
                        <range>
                                <min> -50 </min>
                                <max>  50 </max>
                        </range>
                </aerosurface_scale>
                <pure_gain name="fcs/pitch-gain-1">
                        <input> fcs/pitch-command-scale </input>
                        <gain> -0.36 </gain>
                </pure_gain>
        </channel>
</flight_control>

The first component above (fcs/pitch-trim-sum) takes input from two places, the known static properties, fcs/elevator-cmd-norm and fcs/pitch-trim-cmd-norm. The next component takes as input the output from the first component. The input property listed for the second component is fcs/pitch-trim-sum. Continuing with the above case shows that the last component, fcs/pitch-gain-1, takes as input the output from the preceding component, fcs/pitch-command-scale, which is given the property name, fcs/pitch-command-scale.

So, now we have a way to access many parameters inside JSBSim. We know how the FCS is assembled in JSBSim. The same components used in the FCS are also available to build an autopilot, or other system.

Example notebook

See notebook: Test ballx and 4D table.

Test ballx and 4D table

In [1]:
import numpy as np
from scipy.interpolate import LinearNDInterpolator
import matplotlib.pyplot as plt

import jsbsim

jsbsim.FGJSBBase().debug_lvl = 0
AIRCRAFT_NAME="ballx"
PATH_TO_JSBSIM_FILES=None
fdm = jsbsim.FGFDMExec(PATH_TO_JSBSIM_FILES)
is_aircraft_loaded = fdm.load_model(AIRCRAFT_NAME)

if not is_aircraft_loaded:
    raise RuntimeError(f"Failed to load aircraft model '{AIRCRAFT_NAME}'")
else:
    print(f"Successfully loaded aircraft model '{AIRCRAFT_NAME}'")
    
    # Initialize dummy1 properties to avoid "property does not exist" error
    # when fdm.run() evaluates all aerodynamic functions.
    fdm['aero/dummy1/row-val'] = 0.0
    fdm['aero/dummy1/col-val'] = 0.0
In file /home/vscode/.local/lib/python3.11/site-packages/jsbsim/aircraft/ballx/ballx.xml: line 128
No direction element specified in force object. Default is (0,0,0).

  The aerodynamic moment axis system has been set by default to the bodyXYZ system.
Successfully loaded aircraft model 'ballx'

Show a table <function name="aero/dummy4D"> from ballx.xml

In [2]:
from pathlib import Path
import xml.etree.ElementTree as ET

PATH_TO_JSBSIM_AIRCRAFT = fdm.get_aircraft_path()
print(f"Path to JSBSim aircraft directory: {PATH_TO_JSBSIM_AIRCRAFT}")

xml_path = (Path(PATH_TO_JSBSIM_AIRCRAFT) / AIRCRAFT_NAME / f"{AIRCRAFT_NAME}.xml").resolve()
    
root = ET.parse(xml_path).getroot()
dummy4d = root.find(".//function[@name='aero/dummy4D']")

if dummy4d is None:
    print("Function aero/dummy4D not found")
else:
    print("Function aero/dummy4D found. Showing the whole XML definition:\n======================\n")
    xml_text = ET.tostring(dummy4d, encoding="unicode")
    print('\t' + xml_text)
Path to JSBSim aircraft directory: /home/vscode/.local/lib/python3.11/site-packages/jsbsim/aircraft
Function aero/dummy4D found. Showing the whole XML definition:
======================

	<function name="aero/dummy4D">
          <description>
            Example 4-dimensional lookup table
          </description>
          <product>
            <value> 1.0 </value>
            <table>
              <independentVar lookup="row">aero/alpha-deg</independentVar>                
              <independentVar lookup="column">aero/beta-deg</independentVar>              
              <independentVar lookup="table">fcs/flap-pos-deg</independentVar>            
              <independentVar lookup="axis4">fcs/parachute_reef_pos_norm</independentVar> 

              <tableData breakPoint="0.0"> 
                <tableData breakPoint="0"> 
                                -5         0         5
                    -5.0      0.10      0.18      0.24
                     0.0      0.20      0.28      0.34
                     5.0      0.30      0.38      0.44
                    10.0      0.40      0.48      0.54
                </tableData>
                <tableData breakPoint="20"> 
                                -5         0         5
                    -5.0      5.22      5.30      5.36
                     0.0      5.32      5.40      5.46
                     5.0      5.42      5.50      5.56
                    10.0      5.52      5.60      5.66
                  </tableData>
                </tableData>
              <tableData breakPoint="1.0"> 
                <tableData breakPoint="0"> 
                                -5         0         5
                    -5.0      2.15      2.23      2.29
                     0.0      2.25      2.33      2.39
                     5.0      2.35      2.43      2.49
                    10.0      2.45      2.53      2.59
                </tableData>
                <tableData breakPoint="20"> 
                                -5         0         5
                    -5.0     10.27     10.35     10.41
                     0.0     10.37     10.45     10.51
                     5.0     10.47     10.55     10.61
                    10.0     10.57     10.65     10.71
                </tableData>
              </tableData>
            </table>
          </product>
        </function>

        

Import matrices from <tableData> blocks

In [3]:
# Parse <function name="aero/dummy4D"> and import each leaf <tableData> as a 2D matrix.
# Also keep independentVar lookup values in separate arrays.

def parse_2d_table_block(tabledata_element):
    lines = [ln.strip() for ln in (tabledata_element.text or "").splitlines() if ln.strip()]
    if not lines:
        raise ValueError("Empty 2D <tableData> block")

    # First line: column axis values. Next lines: row axis value + matrix row values.
    axis2_values = np.array([float(v) for v in lines[0].split()], dtype=float)
    row_entries = [[float(v) for v in ln.split()] for ln in lines[1:]]

    axis1_values = np.array([row[0] for row in row_entries], dtype=float)
    matrix_2d = np.array([row[1:] for row in row_entries], dtype=float)

    return axis1_values, axis2_values, matrix_2d


table_node = dummy4d.find(".//table")
if table_node is None:
    raise RuntimeError("No <table> node found in aero/dummy4D")

# independentVar definitions (lookup attribute -> property path)
independent_vars = {
    iv.get("lookup", "row"): (iv.text or "").strip()
    for iv in table_node.findall("independentVar")
}

# axis4 breakpoints and axis3 breakpoints from nested tableData blocks
axis4_values = []
axis3_values = []

# Store each 2D table matrix keyed by (axis4_breakpoint, axis3_breakpoint)
table2d_by_axes = {}

for axis4_block in table_node.findall("tableData"):
    bp4 = float(axis4_block.get("breakPoint"))
    axis4_values.append(bp4)

    for axis3_block in axis4_block.findall("tableData"):
        bp3 = float(axis3_block.get("breakPoint"))
        axis3_values.append(bp3)

        a1_vals, a2_vals, matrix2d = parse_2d_table_block(axis3_block)
        table2d_by_axes[(bp4, bp3)] = matrix2d

# Unique/sorted axis arrays for independent variables
axis4_values = np.array(sorted(set(axis4_values)), dtype=float)
axis3_values = np.array(sorted(set(axis3_values)), dtype=float)
axis1_values = a1_vals
axis2_values = a2_vals

print("independent_vars:", independent_vars)
print("axis1_values (row):", axis1_values)
print("axis2_values (column):", axis2_values)
print("axis3_values (table):", axis3_values)
print("axis4_values:", axis4_values)
print()
print("Imported 2D matrices:")
for (bp4, bp3), matrix2d in sorted(table2d_by_axes.items()):
    print(f"axis4={bp4}, axis3={bp3}, shape={matrix2d.shape}")
    print(matrix2d)
    print()
independent_vars: {'row': 'aero/alpha-deg', 'column': 'aero/beta-deg', 'table': 'fcs/flap-pos-deg', 'axis4': 'fcs/parachute_reef_pos_norm'}
axis1_values (row): [-5.  0.  5. 10.]
axis2_values (column): [-5.  0.  5.]
axis3_values (table): [ 0. 20.]
axis4_values: [0. 1.]

Imported 2D matrices:
axis4=0.0, axis3=0.0, shape=(4, 3)
[[0.1  0.18 0.24]
 [0.2  0.28 0.34]
 [0.3  0.38 0.44]
 [0.4  0.48 0.54]]

axis4=0.0, axis3=20.0, shape=(4, 3)
[[5.22 5.3  5.36]
 [5.32 5.4  5.46]
 [5.42 5.5  5.56]
 [5.52 5.6  5.66]]

axis4=1.0, axis3=0.0, shape=(4, 3)
[[2.15 2.23 2.29]
 [2.25 2.33 2.39]
 [2.35 2.43 2.49]
 [2.45 2.53 2.59]]

axis4=1.0, axis3=20.0, shape=(4, 3)
[[10.27 10.35 10.41]
 [10.37 10.45 10.51]
 [10.47 10.55 10.61]
 [10.57 10.65 10.71]]

Utility function extracting interpolated data via fdm

In [4]:
# Utility function to perform lookup in the 4D table
def jsbsim_lookup4D(row, col, axis3=0.0, axis4=0.0):
    fdm['aero/alpha-deg'] = row
    fdm['aero/beta-deg'] = col
    fdm['fcs/flap-pos-deg'] = axis3
    fdm['fcs/parachute_reef_pos_norm'] = axis4
    fdm.run()
    return fdm['aero/dummy4D']

Plots the surfaces

In [5]:
# Plot all imported 2D surfaces as wireframes of axis_1 vs axis_2
import math

if not table2d_by_axes:
    raise RuntimeError("No 2D matrices found in table2d_by_axes")

# Build axis grid (matrix shape is len(axis1_values) x len(axis2_values))
A1, A2 = np.meshgrid(axis1_values, axis2_values, indexing="ij")

surfaces = sorted(table2d_by_axes.items())
n = len(surfaces)
ncols = 2 if n > 1 else 1
nrows = math.ceil(n / ncols)

fig = plt.figure(figsize=(7 * ncols, 5 * nrows))

x_min, x_max = float(np.min(axis1_values)), float(np.max(axis1_values))
y_min, y_max = float(np.min(axis2_values)), float(np.max(axis2_values))

for i, ((bp4, bp3), Z) in enumerate(surfaces, start=1):
    ax = fig.add_subplot(nrows, ncols, i, projection="3d")
    # Semi-transparent shaded surface.
    ax.plot_surface(A1, A2, Z, alpha=0.5, color="tab:blue", linewidth=0, antialiased=True)
    # Wireframe on top of the shaded surface.
    ax.plot_wireframe(A1, A2, Z, color="tab:blue")
    # Plot tabular breakpoint samples as dots on top of the wireframe.
    ax.scatter(A1.ravel(), A2.ravel(), Z.ravel(), color="k", s=28)

    # Interpolated lookup at row=1, col=1 for the current (axis3, axis4) slice.
    x0, y0 = 1.0, 1.0
    ax3 = 20.0
    ax4 = 1.0
    z_ = float(np.asarray(jsbsim_lookup4D(row=x0, col=y0, axis3=ax3, axis4=ax4)))
    ax.scatter([x0], [y0], [z_], 
               color="red", s=120, edgecolor="k", linewidth=0.7, alpha=1.0)
    ax.text(x0, y0, z_*1.10, f"  ({x0:.1f}, {y0:.1f}, {ax3:.1f}, {ax4:.1f}; {z_:.3f})", 
            ha="center", va="bottom",
            bbox=dict(facecolor='yellow', alpha=0.3),
            color="red", fontsize=9, weight="bold", alpha=1.0)

    # Dashed projection lines:
    # 1) from 3D point to the (axis1, axis2) plane at z = 0
    # 2) in-plane projections extended to plane limits along axis1 and axis2
    z_plane = 0
    ax.plot([x0, x0], [y0, y0], [z_, z_plane], "r--", linewidth=1.5)
    ax.plot([x_min, x_max], [y0, y0], [z_plane, z_plane], "r--", linewidth=1.2)
    ax.plot([x0, x0], [y_min, y_max], [z_plane, z_plane], "r--", linewidth=1.2)

    # Interpolated lookup at row=-1, col=-1 for the current (axis3, axis4) slice.
    x0, y0 = -1.0, -1.0
    ax3 = 20.0
    ax4 = 0.0
    z_ = float(np.asarray(jsbsim_lookup4D(row=x0, col=y0, axis3=ax3, axis4=ax4)))
    ax.scatter([x0], [y0], [z_], 
               color="magenta", s=120, edgecolor="k", linewidth=0.7, alpha=1.0)
    ax.text(x0, y0, z_*1.10, f"  ({x0:.1f}, {y0:.1f}, {ax3:.1f}, {ax4:.1f}; {z_:.3f})", 
            ha="center", va="bottom",
            bbox=dict(facecolor='yellow', alpha=0.3),
            color="magenta", fontsize=9, weight="bold", alpha=1.0)


    # Dashed projection lines:
    # 1) from 3D point to the (axis1, axis2) plane at z = 0
    # 2) in-plane projections extended to plane limits along axis1 and axis2
    z_plane = 0
    ax.plot([x0, x0], [y0, y0], [z_, z_plane], "r--", linewidth=1.5)
    ax.plot([x_min, x_max], [y0, y0], [z_plane, z_plane], "r--", linewidth=1.2)
    ax.plot([x0, x0], [y_min, y_max], [z_plane, z_plane], "r--", linewidth=1.2)

    ax.set_xlabel("axis1 (row)")
    ax.set_ylabel("axis2 (column)")
    ax.set_zlabel("value")
    ax.set_zlim(0.0, 12.0)
    ax.view_init(elev=15, azim=35)
    # In 3D plots, lowering y is usually more effective than changing pad.
    ax.set_title(
        f"axis3={bp3}, axis4={bp4}",
        y=0.98,
        bbox={"facecolor": "#f4f4f4", "edgecolor": "none", "pad": 2.0},
    )

plt.tight_layout(h_pad=3.0)
plt.show()
No description has been provided for this image