Experiment 2: Efficient Search for the Saturation Point

Note

Experiment 2 re-uses the campaign (and also existing simulations) from experiment 1; Especially, the same config.py is used. Therefore, we recommend to continue the experiment in the directory myFirstCampaign/experiment1, and not to create a new sub-campaign using ./playground.py preparecampaign ../myFirstCampaign.

We have seen during Experiment 1 that a manual search for the saturation point by selecting different offered traffic values can take some time, depending on the initial estimation of the user.

With the help of the database, the search for the saturation point can be done in a much more efficient way than by selecting “random” values for the offered traffic and simulating until the point is found. A typical offered traffic vs. throughput curve will always be a bisector in the beginning, reach the saturation point and then, depending on the type of system under simulation, flatten or decrease. Therefore, this saturation point can be found efficiently using binary search: Starting with a small offered traffic as initial value, simulations are run sequentially, doubling the offered traffic every time.

If the throughput is less than the offered traffic, the upper bound is found, and the binary search continues with the mean value of the upper- and the lower bound. This procedure continues until the upper- and lower bound have converged.

As this search for the saturation throughput is needed often, it is directly encapsulated into the parameter generation process as described in the previous experiment.

We now have to distinguish between two types of parameters:

Scenario parameters
Parameters to distinguish different scenario aspects, e.g. distance between two nodes
Input parameter
The input to which the saturation point of the function f(input) = output shall be found.

In the following, the existing file campaignConfiguration.py is changed to implement the binary search. An example implementation can be found in the directory openWNS/tests/system/wifimac-tests/PyConfig/experiment2

Parameter Class

The parameter class recognises the scenario parameters by a new parameter parameterRange; only one parameter (the input parameter) must be without this parameterRange, but with the default (i.e. starting value) instead. Hence, the parameter class from the first experiment would look like the following:

from openwns.wrowser.simdb.Parameters import AutoSimulationParameters, Parameters, Bool, Int, Float, String
import openwns.wrowser.Configuration as config
import openwns.wrowser.simdb.Database as db
import subprocess

class Set(AutoSimulationParameters):
    # scenario parameters
    simTime = Float(parameterRange = [5.0])
    distance = Float(parameterRange = [25.0])
    ulRatio = Float(parameterRange = [0.0])
    packetSize = Int(parameterRange = [1480*8, 80*8])

    # input parameter
    offeredTraffic = Int(default = 100000)

Database Access

The next step is to define a function that returns the output value - the throughput in our case. To be compatible, this function must have a defined signature and return value:

def getTotalThroughput(paramsString, inputName, cursor):
    myQuery = " \
       SELECT idResults.scenario_id, idResults." + inputName + ", VAL.mean \
       FROM moments VAL, (SELECT scenario_id, " + inputName + " FROM parameter_sets \
                          WHERE " + paramsString + ") AS idResults \
       WHERE VAL.scenario_id = idResults.scenario_id AND \
       	     VAL.alt_name = 'ip.endToEnd.window.incoming.bitThroughput_Moments' \
       ORDER BY idResults." + inputName + ";"
    cursor.execute(myQuery)
    resultsIn = cursor.fetchall()

    myQuery = " \
       SELECT idResults.scenario_id, idResults." + inputName + ", VAL.mean \
       FROM moments VAL, (SELECT scenario_id, " + inputName + " FROM parameter_sets \
                          WHERE " + paramsString + ") AS idResults \
       WHERE VAL.scenario_id = idResults.scenario_id AND \
             VAL.alt_name = 'ip.endToEnd.window.aggregated.bitThroughput_Moments' \
       ORDER BY idResults." + inputName + ";"
    cursor.execute(myQuery)
    resultsAgg = cursor.fetchall()

    results = []
    for i in zip(resultsAgg, resultsIn):
        agg = i[0]
        inc = i[1]
        assert(agg[0] == inc[0])
        assert(agg[1] == inc[1])
        results.append([agg[0], agg[1], agg[2] + inc[2]])

    return results

This function works in the following way: As input, it gets

  • paramsString A string containing an SQL-ready enumeration of the parameters which define a specific scenario
  • inputName The name of the input variable in the database
  • cursor A cursor object to access the database

This input is used to start two queries: The first one gets a list of 3-tuples, containing the scenario-id, the value of the input name and the mean value of the moment probe with the name ip.endToEnd.window.incoming.bitThroughput_Moments. The inner SELECT - statement is only used to find the correct scenario-id, using the paramsString.

Similarly, the second query gets the same 3-tuple, but with the mean value of the probe ip.endToEnd.window.aggregated.bitThroughput_Moments. The remaining lines go through both lists (at the same time using the zip command), compare the scenario-id and the value of the input parameter and append another 3-tuple, consisting of the scenario-id, the input value and the sum of the incoming and aggregated traffic - which should be the same as the input value in our case.

Accessing the Cursor

With the following lines, the cursor from the appropriate database belonging to the simulation campaign is fetched:

conf = config.Configuration()
conf.read("./.campaign.conf")
db.Database.connectConf(conf)
cursor = db.Database.getCursor()