Difference between revisions of "Simulation Extensions"

From OpenRocket wiki
Jump to navigation Jump to search
Line 377: Line 377:
 
|}
 
|}
  
After some boilerplate, this class creates a new <code>DoubleModel</code> to manage the airstart altitude.  The most thing to notice about the constructor is the parameter <code>"LaunchAltitude"</code>.  This has to match the names of the <code>getLaunchAltitude()</code> and <code>setLaunchAltitude()</code> methods mentioned earlier; the system uses this string to synthesize calls to those methods.
+
After some boilerplate, this class creates a new <code>DoubleModel</code> to manage the airstart altitude.  The most things to notice about the <code>DoubleModel</code> constructor are the parameters <code>"LaunchAltitude"</code> and <code>UnitGroup.UNITS_DISTANCE</code>.
 +
* <code>"LaunchAltitude"</code> is used by the system to synthesize calls to the <code>getLaunchAltitude()</code> and <code>setLaunchAltitude()</code> methods mentioned earlier; of course, they have to match.
 +
* <code>UnitGroup.UNITS_DISTANCE</code> specifies the unit group to be used by this <code>DoubleModel</code>. OpenRocket uses SI (MKS) units internally, but allows users to select the units they wish to use for their interface.  Specifying a <code>UnitGroup</code> provides the conversions and unit displays for the interface.  The available <code>UnitGroup</code>s are defined in <code>core/src/net/sf/openrocket/unit/UnitGroup.java</code>
  
The remaining code in this method creates a JSpinner, a UnitSelector, and a BasicSlider all referring to this DoubleModel.  When the resulting configurator is displayed, it looks like this:
+
The remaining code in this method creates a <code>JSpinner</code>, a <code>UnitSelector</code>, and a <code>BasicSlider</code> all referring to this DoubleModel.  When the resulting configurator is displayed, it looks like this:
  
 
[[File:Example_Configurator.png]]
 
[[File:Example_Configurator.png]]

Revision as of 01:19, 13 December 2022

By using OpenRocket's extension and listener mechanism, it's possible to modify the program itself to add features that are not supported by the program as distributed; some extensions that have been created already provide the ability to air-start a rocket, to add active roll control, and to calculate and save extra flight data.

This page will discuss extensions and simulations. We'll start by showing how a simulation is executed (so you can get a taste of what's possible), and then document the process of creating the extension. WARNING: writing an extension inserts new code into the program. It is entirely possible to disrupt a simulation in a way that invalidates simulation results, or can even crash the program. Be careful!

Adding an Existing Extension to a Simulation

Extensions are added to a simulation through a menu in the "Simulation Options" tab.

  1. Open a .ork file and go to the Flight Simulations tab
  2. Click the Edit simulation button to open the Edit simulation dialog.
  3. Go to the Simulation options tab.
  4. Click the Add extension button

This will open a menu similar to the one in the following screenshot:

Extension-menu.png

Clicking on the name of an extension will add it to the simulation; if it has a configuration dialog the dialog will be opened:

Air-start-configuration.png

In the case of the air-start extension, the configuration dialog allows you to set the altitude and velocity at which your simulation will begin. After you close the configuration dialog (if any), a new panel will be added to the Simulation options pane, showing the new extension with buttons to reconfigure it, obtain information about it, or remove it from the simulation:

Air-start-pane.png

Creating a New OpenRocket Extension

Before we can discuss writing an extension, we need to briefly discuss some of the internals of OpenRocket. In particular, we need to talk about the simulation status, flight data, and simulation listeners.

Simulation Status

As a simulation proceeds, it maintains its state in a SimulationStatus. The SimulationStatus object contains information about the rocket's current position, orientation, velocity and simulation state. It also contains a reference to a copy of the rocket design and its configuration. Any simulation listener method may modify the state of the rocket by changing the properties of the SimulationStatus object.

You can obtain current information regarding the state of the simulation by calling get*() methods. For instance, the rocket's current position is returned by calling getRocketPosition(); the rocket's position can be changed by calling setRocketPosition<Coordinate position>. All of the get*() and set*() methods can be found in code/src/net/sf/openrocket/simulation/SimulationStatus.java

Simulation Listeners

Simulation listeners are methods that OpenRocket calls at specified points in the computation to either record information or modify the simulation state. These are divided into three interface classes, named SimulationListener, SimulationComputationListener, and SimulationEventListener.

All of these interfaces are implemented by the abstract class AbstractSimulationListener. This class provides empty methods for all of the methods defined in the three interfaces, which are overridden as needed when writing a listener. A typical listener method (which is actually in the Air-start listener), would be

public void startSimulation(SimulationStatus status) throws SimulationException {
    status.setRocketPosition(new Coordinate(0, 0, getLaunchAltitude()));
    status.setRocketVelocity(status.getRocketOrientationQuaternion().rotate(new Coordinate(0, 0, getLaunchVelocity())));
}

This method is called when the simulation is first started. It obtains the desired launch altitude and velocity from its configuration, and inserts them into the simulation status.

The full set of listener methods, with documentation regarding when they are called, can be found in core/src/net/sf/openrocket/AbstractSimulationListener.java.

The listener methods can have three return value types:

  • The startSimulation, endSimulation, and postStep are called at a specific point of the simulation. They are void methods and do not return any value.
  • The preStep and event-related hooks return a boolean value indicating whether the action should be taken or not. A return value of true indicates that the action should be taken as normally would be (default), false will inhibit the action.
  • The pre- and post-computation methods may return the computed value, either as an object or a double value. The pre-computation methods allow pre-empting the entire computation, while the post-computation methods allow augmenting the computed values. These methods may return null or Double.NaN to use the original values (default), or return an overriding value.

Every listener receives a SimulationStatus (see above) object as the first argument, and may have additional arguments.

Each listener method may also throw a SimulationException. This is considered an error during simulation, and an error dialog is displayed to the user with the exception message. The simulation data thus far is not stored in the simulation. Throwing a RuntimeException is considered a bug in the software and will result in a bug report dialog.

If a simulation listener wants to stop a simulation prematurely without an error condition, it needs to add a flight event of type FlightEvent.SIMULATION_END to the simulation event queue:

 status.getEventQueue().add(new FlightEvent(FlightEvent.Type.SIMULATION_END, status.getSimulationTime(), null));

This will cause the simulation to be terminated normally.

Creating a New Simulation Extension

Creating an extension for OpenRocket requires writing three classes:

  • A listener, which extends AbstractSimulationListener. This will be the bulk of your extension, and performs all the real work.
  • An extension, which extends AbstractSimulationExtension. This inserts your listener into the simulation when it is called. Your listener can (and probably will) be private within your extension.
  • A provider, which extends AbstractSimulationExtensionProvider This puts your extension into the menu described above.

In addition, if your extension will have a configuration GUI, you will need to write:

  • A configurator, which extends AbstractSwingSimulationExtensionConfigurator<E>

You can either create your extension outside the source tree and insert it in OpenRocket's .jar file when compiled, or you can insert it in the source tree and compile it with OpenRocket. Since all of OpenRocket's code is freely available, and reading the code for the existing extensions will be very helpful in writing your's, the easiest approach is to simply insert it in the source tree. If you select this option, a very logical place to put your extension is in

core/src/net/sf/openrocket/simulation/extension/example/

This is where the extensions provided with OpenRocket are defined. Your configurator, if any, will logically go in

swing/src/net/sf/openrocket/simulation/extension/example/

Extension Example

To make things concrete, we'll start by creating a simple example extension, to air-start a rocket from a hard-coded altitude. Later, we'll add a configurator to the extension so we can set the launch altitude through a GUI at run time.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package net.sf.openrocket.simulation.extension.example;

import net.sf.openrocket.simulation.SimulationStatus;
import net.sf.openrocket.simulation.exception.SimulationException;
import net.sf.openrocket.simulation.extension.AbstractSimulationExtension;
import net.sf.openrocket.simulation.listeners.AbstractSimulationListener;
import net.sf.openrocket.util.Coordinate;

/**
 * Simulation extension that launches a rocket from a specific altitude.
 */
public class AirStartExample extends AbstractSimulationExtension {

    public void initialize(SimulationConditions conditions) throws SimulationException {
        conditions.getSimulationListenerList().add(new AirStartListener());
    }

    @Override
    public String getName() {
        return "Air-Start Example";
    }

    @Override
    public String getDescription() {
        return "Simple extension example for air-start";
    }

    private class AirStartListener extends AbstractSimulationListener {

        @Override
        public void startSimulation(SimulationStatus status) throws SimulationException {
            status.setRocketPosition(new Coordinate(0, 0, 1000.0));
        }
    }
}

There are several important features in this example:

  • The initialize() method, which adds the listener to the List of simulation listeners. This is the only method that is required to be defined in your extension.
  • The getName() method, which provides the extension's name. A default getName() is provided by AbstractSimulationExtension, which simply uses the classname (so for this example,

getName() would return "AirStartExample" if hadn't overridden it).

  • The getDescription() method, which provides a brief description of the purpose of the extension. This is the method that provides the text for the an Info button in the first section of this page.
  • The listener itself, which provides a single startSimulation() method. When the simulation for starts executing, this listener is called and the rocket is set to an altitude of 1000 meters.

This will create the extension when it's compiled, but it won't put it in the simulation extension menu (so it'll be pretty much useless!). To be able to actually use it, we need a provider, like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package net.sf.openrocket.simulation.extension.example;

import net.sf.openrocket.plugin.Plugin;
import net.sf.openrocket.simulation.extension.AbstractSimulationExtensionProvider;

@Plugin
public class AirStartExampleProvider extends AbstractSimulationExtensionProvider {
    public AirStartExampleProvider() {
        super(AirStartExample.class, "Launch conditions", "Air-start example");
    }
}

This class adds your extension to the extension menu. The first String ("Launch Conditions") is the first level, while the second ("Air-start example") is the actual menu entry. These strings can be anything you want; using a first level entry that didn't previously exist will add it to the first level menu.

Try it! Putting the extension in a file named core/src/net/sf/openrocket/simulation/extensions/example/AirStartExample.java and the provider in core/src/net/sf/openrocket/simulation/extensions/example/AirStartExampleProvider.java, compiling, and running will give you a new entry in the extensions menu; adding it to the simulation will cause your simulation to start at an altitude of 1000 meters.

Adding a Configurator

To be able to configure the extension at run time, we need to write a configurator and provide it with a way to communicate with the extension, first, we'll modify the extension as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package net.sf.openrocket.simulation.extension.example;

import net.sf.openrocket.simulation.SimulationStatus;
import net.sf.openrocket.simulation.exception.SimulationException;
import net.sf.openrocket.simulation.extension.AbstractSimulationExtension;
import net.sf.openrocket.simulation.listeners.AbstractSimulationListener;
import net.sf.openrocket.util.Coordinate;

/**
 * Simulation extension that launches a rocket from a specific altitude.
 */
public class AirStartExample extends AbstractSimulationExtension {

    public void initialize(SimulationConditions conditions) throws SimulationException {
        conditions.getSimulationListenerList().add(new AirStartListener());
    }

    @Override
    public String getName() {
        return "Air-Start Example";
    }

    @Override
    public String getDescription() {
        return "Simple extension example for air-start";
    }

    public double getLaunchAltitude() {
        return config.getDouble("launchAltitude", 1000.0);
    }

    public void setLaunchAltitude(double launchAltitude) {
        config.put("launchAltitude", launchAltitude);
        fireChangeEvent();
    }
        
    private class AirStartListener extends AbstractSimulationListener {

        @Override
        public void startSimulation(SimulationStatus status) throws SimulationException {

            status.setRocketPosition(new Coordinate(0, 0, getLaunchAltitude()));
        }
    }
}

This adds two methods to the extension (getLaunchAltitude() and setLaunchAltitude()), and calls getLaunchAltitude() from within the listener to obtain the configured launch altitude. config is a Config object, provided by AbstractSimulationExtension.

One thing to notice is that the call to config.getDouble(), the value 1000.0 is the default airstart altitude (in meters). This is the value that will appear when the configurator is first opened.

The configurator itself looks like this

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package net.sf.openrocket.simulation.extension.example;

import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;

import net.sf.openrocket.document.Simulation;
import net.sf.openrocket.gui.SpinnerEditor;
import net.sf.openrocket.gui.adaptors.DoubleModel;
import net.sf.openrocket.gui.components.BasicSlider;
import net.sf.openrocket.gui.components.UnitSelector;
import net.sf.openrocket.plugin.Plugin;
import net.sf.openrocket.simulation.extension.AbstractSwingSimulationExtensionConfigurator;
import net.sf.openrocket.unit.UnitGroup;

@Plugin
public class AirStartConfigurator extends AbstractSwingSimulationExtensionConfigurator<AirStart> {
	
    public AirStartConfigurator() {
        super(AirStart.class);
    }
	
    @Override
    protected JComponent getConfigurationComponent(AirStart extension, Simulation simulation, JPanel panel) {
        panel.add(new JLabel("Launch altitude:"));

        DoubleModel m = new DoubleModel(extension, "LaunchAltitude", UnitGroup.UNITS_DISTANCE, 0);

        JSpinner spin = new JSpinner(m.getSpinnerModel());
        spin.setEditor(new SpinnerEditor(spin));
        panel.add(spin, "w 65lp!");

        UnitSelector unit = new UnitSelector(m);
        panel.add(unit, "w 25");

        BasicSlider slider = new BasicSlider(m.getSliderModel(0, 5000));
        panel.add(slider, "w 75lp, wrap");
		
        return panel;
    }
}

After some boilerplate, this class creates a new DoubleModel to manage the airstart altitude. The most things to notice about the DoubleModel constructor are the parameters "LaunchAltitude" and UnitGroup.UNITS_DISTANCE.

  • "LaunchAltitude" is used by the system to synthesize calls to the getLaunchAltitude() and setLaunchAltitude() methods mentioned earlier; of course, they have to match.
  • UnitGroup.UNITS_DISTANCE specifies the unit group to be used by this DoubleModel. OpenRocket uses SI (MKS) units internally, but allows users to select the units they wish to use for their interface. Specifying a UnitGroup provides the conversions and unit displays for the interface. The available UnitGroups are defined in core/src/net/sf/openrocket/unit/UnitGroup.java

The remaining code in this method creates a JSpinner, a UnitSelector, and a BasicSlider all referring to this DoubleModel. When the resulting configurator is displayed, it looks like this:

Example Configurator.png

Flight Data

The results from executing a simulation is stored in a Assuming the simulation status is stored in status, we obtain the flight data for the currently executing simulation branch by calling status.getFlightData().

OpenRocket refers to simulation variables as FlightDataTypes, which are List<Double>s, with one list for each simulation variable. To obtain a FlightDataType, for example the current motor mass, from flightData, we call flightData.get(FlightDataType.TYPE_MOTOR_MASS)). The standard FlightDataTypes are all created in core/src/net/sf/openrocket/simulation/FlightDataType.java; the mechanism for creating a new FlightDataType for your extension will be described later.

Data from the current simulation step can be obtained with e.g. flightData.getLast(FlightDataType.TYPE_MOTOR_MASS).