This tutorial should give a first impression what simulation can do, what business cases are behind and how this is implemented in jBPM.
Download & Status
To get started you need:
- Source code for this tutorial
- A jBPM jPDL later than 3.2.2, for example jbpm 3.2.3.
- The jbpm-simulation.jar
- Maybe you want the javadocs: jpm-simulation-javadoc.zip or the simulation source code: jbpm-simulation-src.jar
- Additionally to the default jBPM dependencies (please check the offical jBPM documentation for these) you need
- The desmoj-library: http://repository.jboss.com/desmoj/2.1.1/lib/desmoj.jar
- If you want to open the JasperReports reports, you also need the JasperReports libraries:
The sources for the tutorial are also checked in the jBPM CVS repository, so you can also checkout the HEAD revision there, then you get everything you need automatically.
Database
The first part of this tutorial needs a database, because we want to retrieve historical information out of it. This includes calculating the standard deviation, which can be done by most of the databases today (see Howto enable MySQL standard deviation calculation in Hibernate). Unfortunately, some of the database queries made trouble with the standard hibernate HQL. So at the moment I provide only a version, running on MySQL 5! I work on an optimized version for Oracle and a database independent one, which will be slower.
But you can also skip the historical data part and start with the simulation right away, this doesn’t need any database!
The business process
In this tutorial we will look at an easy business process: The handling of returned goods from customers. Think about a webshop for technical stuff, where the customers can send back the goods they ordered within two weeks without any reasons (at least in Germany) or within the warranty, if there is a defect. Typically, the goods are checked from the webshop team, if they are really broken. Let’s assume that our shop makes a quick check after goods arrival, “easy” defects can be approved at this early stage already, but a lot of defects can only be diagnosed with an extended test and additional test equipment. If the defect cannot even be approved with that test, the stuff is send back to the customer, it may not be broken.
There is one additional requirement if the customer returns his shopping within two weeks (again: at least in Germany): The shipping costs must be paid for him. So we have to transfer the money to the customer’s bank account in this case.
Modeled in JBoss jBPM with jPDL, this process could look like the following diagram:
The jPDL sources (the process as XML file) can be downloaded here. A good idea is also to check the sources for the tutorial which are available via CVS in jbpm 3 HEAD, because there will be ongoing development in the area of the simulation.
Historical data – Running our process in an application
Let’s assume this process was deployed in a real life application, it worked nice and we can access jbpm log data for it. For this tutorial, I have written a small Java program, which executes a bunch of process instance taking different “paths” through the process. This is not a part of the simulation, but produces a faked history for us. As an example I show the code to walk the path “ordered within last two weeks – quick test approved defect”:
ProcessInstance pi = pd.createProcessInstance(); pi.getContextInstance().setVariable("decisionOne", "YES"); pi.getRootToken().signal(); // start state simulateRandomTime(); TaskInstance ti1 = (TaskInstance) pi.getTaskMgmtInstance() .getTaskInstances().iterator().next(); ti1.start("me"); // task "transfer shipping costs" simulateRandomTime(); ti1.end(); // task "transfer shipping costs" simulateRandomTime(); pi.getRootToken().signal(); // state "wait for parcel" simulateRandomTime(); TaskInstance ti2 = (TaskInstance) pi.getTaskMgmtInstance(). getUnfinishedTasks(pi.getRootToken()).iterator().next(); ti2.start("me"); // task "quick test" simulateRandomTime(); ti2.end("defect approved"); // task "quick test" simulateRandomTime(); TaskInstance ti3 = (TaskInstance) pi.getTaskMgmtInstance() .getUnfinishedTasks(pi.getRootToken()).iterator().next(); ti3.start("me"); // task "refund" simulateRandomTime(); ti3.end(); // task "refund" ctx.save(pi);
Other process paths can be programmed in such a way too. Again: This is only to “generate” historical data for our tutorial. Normally the historical data is automatically written when people use your application, where the process is deployed into. The whole code is available in the “CreateHistoricalData” class.
After creating the historical data, we can use the org.jbpm.sim.GetSimulationInputCommand to retrieve statistics about it. The command simply calculate the statistical figures and returns a BamSimulationProposal. This proposal can be printed on the console, this is the result I got for statistical data after running our hack:
------ PROCESS ReturnDefectiveGoods / Version: 0 --------- start event sample count = 195 start event interval min = 4.0 start event interval max = 183.0 start event interval avg = 94.3247 start event interval stddev = 52.8106 ------ STATEs --------- wait for parcel: sample count = 195 duration min = 1.0 duration max = 60.0 duration avg = 28.63671795 duration stddev = 17.57688127 -> parcel arrived: 195 ------ DECISIONs --------- ordered within the last two weeks?: sample count = 195 duration min = 0.0 duration max = 0.0 duration avg = 0.0 duration stddev = 0.0 -> YES: 100 -> NO: 95 ordered within the last two weeks?: sample count = 195 duration min = 0.0 duration max = 0.0 duration avg = 0.0 duration stddev = 0.0 -> YES: 100 -> NO: 95 ------ TASKs --------- transfer shipping costs: sample count = 100 duration min = 7.0 duration max = 303.0 duration avg = 180.03 duration stddev = 78.3107 -> done: 100 quick test: sample count = 195 duration min = 6.0 duration max = 312.0 duration avg = 154.3077 duration stddev = 88.6442 -> no defect: 165 -> defect approved: 30 extended technical test: sample count = 165 duration min = 47.0 duration max = 1507.0 duration avg = 732.2485 duration stddev = 448.1038 -> no defect: 20 -> defect approved: 145 send back goods: sample count = 20 duration min = 15.0 duration max = 603.0 duration avg = 325.5 duration stddev = 182.0718 -> null: 20 refund: sample count = 175 duration min = 7.0 duration max = 304.0 duration avg = 157.48 duration stddev = 83.3658 -> null: 175
This data can serve as input for our simulation project, to simulate the status quo. The jBPM simulation tool offers the possibility, to get this data as scenario configuration XML, similar to the XML configuration shown below. This can be directly used (in your Java code) to create an simulation experiment, but also may be printed on screen (or some web page in the console), so you can copy & paste it into your simulation configuration and change it to whatever you want to.
Here the same result as XML:
<scenario name="status_quo"> <distribution name="ReturnDefectiveGoods.ReturnDefectiveGoods" sample-type="real" type="erlang" mean="94.3247" standardDeviation="52.8106"/> <distribution name="ReturnDefectiveGoods.wait for parcel" sample-type="real" type="normal" mean="28.636717" standardDeviation="17.576881"/> <distribution name="ReturnDefectiveGoods.transfer shipping costs" sample-type="real" type="normal" mean="180.03" standardDeviation="78.3107"/> <distribution name="ReturnDefectiveGoods.quick test" sample-type="real" type="normal" mean="154.3077" standardDeviation="88.6442"/> <distribution name="ReturnDefectiveGoods.extended technical test" sample-type="real" type="normal" mean="732.2485" standardDeviation="448.1038"/> <distribution name="ReturnDefectiveGoods.send back goods" sample-type="real" type="normal" mean="325.5" standardDeviation="182.0718"/> <distribution name="ReturnDefectiveGoods.refund" sample-type="real" type="normal" mean="157.48" standardDeviation="83.3658"/> <resource-pool name="tester" pool-size="1" costs-per-time-unit="1"/> <resource-pool name="clerk" pool-size="1" costs-per-time-unit="1"/> <resource-pool name="accountant" pool-size="1" costs-per-time-unit="1"/> <resource-pool name="dispatcher" pool-size="1" costs-per-time-unit="1"/> <sim-process path="/ReturnDefectiveGoods/processdefinition.xml"> <process-overwrite start-distribution="ReturnDefectiveGoods.ReturnDefectiveGoods"/> <state-overwrite state-name="wait for parcel" time-distribution="ReturnDefectiveGoods.wait for parcel"> <transition name="parcel arrived" probability="195"/> </state-overwrite> <decision-overwrite decision-name="ordered within the last two weeks?"> <transition name="YES" probability="100"/> <transition name="NO" probability="95"/> </decision-overwrite> <decision-overwrite decision-name="ordered within the last two weeks?"> <transition name="YES" probability="100"/> <transition name="NO" probability="95"/> </decision-overwrite> <task-overwrite task-name="transfer shipping costs" time-distribution="ReturnDefectiveGoods.transfer shipping costs"> <transition name="done" probability="100"/> </task-overwrite> <task-overwrite task-name="quick test" time-distribution="ReturnDefectiveGoods.quick test"> <transition name="no defect" probability="165"/> <transition name="defect approved" probability="30"/> </task-overwrite> <task-overwrite task-name="extended technical test" time-distribution="ReturnDefectiveGoods.extended technical test"> <transition name="no defect" probability="20"/> <transition name="defect approved" probability="145"/> </task-overwrite> <task-overwrite task-name="send back goods" time-distribution="ReturnDefectiveGoods.send back goods"> <transition probability="20"/> </task-overwrite> <task-overwrite task-name="refund" time-distribution="ReturnDefectiveGoods.refund"> <transition probability="175"/> </task-overwrite> </sim-process> </scenario>
The sample data here was only to demonstrate the features to retrieve statistics from jBPM and use it as simulation input. Because of the poor algorithm of generating the data, its value is limited. Transferring costs by the accountant for example, should always take duration close to the average. The big standard deviation is not realistic here, it would only make for tasks, which processing times differs a lot from case to case, a good example can be the extended technical test.
For this tutorial I have changed the above figures slightly, mainly to make them a bit easier to read. For a better overview I added the important numbers into the process diagram:
Simulation Use Case 1: Discover staffing strategy
Okay, the process runs and does the job well. As the year move closer to Christmas, your manager starts to think about staffing strategies. Your company has a successful year, so the question he asks you is: “How many people do we need after Christmas time? Because of the sales we have we can forecast the number of returns. Please estimate based on these figures how many people we need without messing the processing time”. Seems to be an easy question, or?
Calculation with Spreadsheet
If we don’t know about simulation, we could use a simple spreadsheet to calculate that figure, let’s assume we have statistics about historical runs available like shown above. I calculated four scenarios:
- The number of people needed for the status quo, if we use the average processing times, the status quo normal case
- The number of people needed for the status quo, if we use average + standard deviation as processing time, let’s call that status quo worst case
- The number of people needed for Christmas, if we use the average processing times, the christmas normal case
- The number of people needed for Christmas, if we use average + standard deviation as processing times, the christmas worst case
Here are the results (or download the Excel file):
The last part of the tables gives an idea how many people we need. The more critical my process, the closer I should go with my staffing strategy towards the worst case scenario to add safety.
Leverage simulation
Totally missing at the moment are dynamic phenomena, which may build up some additional delay. To be honest: In this easy example, these phenomena aren’t too big. But image more complex scenarios with different processes running in parallel consuming the same resources. There it gets quite hard to calculate it without simulation. And on the other hand: using the simulation tool is not really more complicated than the spreadsheet calculcation. In fact, if it is integrated in your out-of-the-box jBPM environment it gets even easier to use!
Okay, let’s make a simulation experiment for the question above. The XML configuration for the basic scenario was copied from the output of the above shown historical data retrieval tool (like mentioned above, I changed the figures a bit to make them easier and more realistic. The scenario XML matches with the figures of the spreadsheet calculation). The scenario configuration uses the possibility to define “abstract” base scenarios. Abstract means, that the execute attribute of a scenarion can be set to false, then the scenario is not simulated. And this scenario can be used as a basis for other scenarios (the whole configuration of the base scenario is read first, and can be overwritten in the scenario itself). This makes the configuration quite readable:
<?xml version="1.0" encoding="UTF-8"?> <!-- experiment definition for jbpm simulation tutorial written by Bernd Ruecker (camunda GmbH) --> <experiment name='ReturnDefectiveGoods' time-unit='second' run-time='28800' real-start-time='30.03.1980 00:00:00:000' currency='€' unutilized-time-cost-factor='0.0'> <!-- 28800 seconds = 8 hours = 1 working day --> <scenario name="status_quo" execute="false"> <distribution name="ReturnDefectiveGoods.start" sample-type="real" type="erlang" mean="95"/> <distribution name="ReturnDefectiveGoods.wait for parcel" sample-type="real" type="normal" mean="28" standardDeviation="17"/> <distribution name="ReturnDefectiveGoods.transfer shipping costs" sample-type="real" type="normal" mean="180" standardDeviation="30"/> <distribution name="ReturnDefectiveGoods.quick test" sample-type="real" type="normal" mean="180" standardDeviation="60"/> <distribution name="ReturnDefectiveGoods.extended technical test" sample-type="real" type="normal" mean="732.2485" standardDeviation="448.1038"/> <distribution name="ReturnDefectiveGoods.send back goods" sample-type="real" type="normal" mean="325.5" standardDeviation="182.0718"/> <distribution name="ReturnDefectiveGoods.refund" sample-type="real" type="normal" mean="180" standardDeviation="30"/> <sim-process path="/ReturnDefectiveGoods/processdefinition.xml"> <process-overwrite start-distribution="ReturnDefectiveGoods.start"/> <state-overwrite state-name="wait for parcel" time-distribution="ReturnDefectiveGoods.wait for parcel"> <transition name="parcel arrived" probability="195"/> </state-overwrite> <decision-overwrite decision-name="ordered within the last two weeks?"> <transition name="YES" probability="100"/> <transition name="NO" probability="95"/> </decision-overwrite> <decision-overwrite decision-name="ordered within the last two weeks?"> <transition name="YES" probability="100"/> <transition name="NO" probability="95"/> </decision-overwrite> <task-overwrite task-name="transfer shipping costs" time-distribution="ReturnDefectiveGoods.transfer shipping costs"> <transition name="done" probability="100"/> </task-overwrite> <task-overwrite task-name="quick test" time-distribution="ReturnDefectiveGoods.quick test"> <transition name="no defect" probability="165"/> <transition name="defect approved" probability="30"/> </task-overwrite> <task-overwrite task-name="extended technical test" time-distribution="ReturnDefectiveGoods.extended technical test"> <transition name="no defect" probability="20"/> <transition name="defect approved" probability="145"/> </task-overwrite> <task-overwrite task-name="send back goods" time-distribution="ReturnDefectiveGoods.send back goods"> <transition probability="20"/> </task-overwrite> <task-overwrite task-name="refund" time-distribution="ReturnDefectiveGoods.refund"> <transition probability="175"/> </task-overwrite> </sim-process> </scenario> <scenario name="status_quo_normal_case" execute="true" base-scenario="status_quo"> <resource-pool name="tester" pool-size="5" costs-per-time-unit="0.025"/> <resource-pool name="clerk" pool-size="2" costs-per-time-unit="0.011111111"/> <resource-pool name="accountant" pool-size="2" costs-per-time-unit="0.022222222"/> <resource-pool name="dispatcher" pool-size="1" costs-per-time-unit="0.013888889"/> </scenario> <scenario name="status_quo_worst_case" execute="true" base-scenario="status_quo"> <resource-pool name="tester" pool-size="5" costs-per-time-unit="0.025"/> <resource-pool name="clerk" pool-size="2" costs-per-time-unit="0.011111111"/> <resource-pool name="accountant" pool-size="3" costs-per-time-unit="0.022222222"/> <resource-pool name="dispatcher" pool-size="1" costs-per-time-unit="0.013888889"/> </scenario> <scenario name="christmas" execute="false"> <distribution name="ReturnDefectiveGoods.start" sample-type="real" type="erlang" mean="55"/> <distribution name="ReturnDefectiveGoods.wait for parcel" sample-type="real" type="normal" mean="28" standardDeviation="17"/> <distribution name="ReturnDefectiveGoods.transfer shipping costs" sample-type="real" type="normal" mean="180" standardDeviation="30"/> <distribution name="ReturnDefectiveGoods.quick test" sample-type="real" type="normal" mean="180" standardDeviation="60"/> <distribution name="ReturnDefectiveGoods.extended technical test" sample-type="real" type="normal" mean="732.2485" standardDeviation="448.1038"/> <distribution name="ReturnDefectiveGoods.send back goods" sample-type="real" type="normal" mean="325.5" standardDeviation="182.0718"/> <distribution name="ReturnDefectiveGoods.refund" sample-type="real" type="normal" mean="180" standardDeviation="30"/> <sim-process path="/ReturnDefectiveGoods/processdefinition.xml"> <process-overwrite start-distribution="ReturnDefectiveGoods.start"/> <state-overwrite state-name="wait for parcel" time-distribution="ReturnDefectiveGoods.wait for parcel"> <transition name="parcel arrived" probability="195"/> </state-overwrite> <decision-overwrite decision-name="ordered within the last two weeks?"> <transition name="YES" probability="100"/> <transition name="NO" probability="95"/> </decision-overwrite> <decision-overwrite decision-name="ordered within the last two weeks?"> <transition name="YES" probability="100"/> <transition name="NO" probability="95"/> </decision-overwrite> <task-overwrite task-name="transfer shipping costs" time-distribution="ReturnDefectiveGoods.transfer shipping costs"> <transition name="done" probability="100"/> </task-overwrite> <task-overwrite task-name="quick test" time-distribution="ReturnDefectiveGoods.quick test"> <transition name="no defect" probability="165"/> <transition name="defect approved" probability="30"/> </task-overwrite> <task-overwrite task-name="extended technical test" time-distribution="ReturnDefectiveGoods.extended technical test"> <transition name="no defect" probability="20"/> <transition name="defect approved" probability="145"/> </task-overwrite> <task-overwrite task-name="send back goods" time-distribution="ReturnDefectiveGoods.send back goods"> <transition probability="20"/> </task-overwrite> <task-overwrite task-name="refund" time-distribution="ReturnDefectiveGoods.refund"> <transition probability="175"/> </task-overwrite> </sim-process> </scenario> <scenario name="christmas_normal_case" execute="true" base-scenario="christmas"> <resource-pool name="accountant" pool-size="5" costs-per-time-unit="0.022222222"/> <resource-pool name="clerk" pool-size="4" costs-per-time-unit="0.011111111"/> <resource-pool name="tester" pool-size="11" costs-per-time-unit="0.025"/> <resource-pool name="dispatcher" pool-size="1" costs-per-time-unit="0.013888889"/> </scenario> <scenario name="christmas_worst_case" execute="true" base-scenario="christmas"> <resource-pool name="accountant" pool-size="6" costs-per-time-unit="0.022222222"/> <resource-pool name="clerk" pool-size="5" costs-per-time-unit="0.011111111"/> <resource-pool name="tester" pool-size="18" costs-per-time-unit="0.025"/> <resource-pool name="dispatcher" pool-size="1" costs-per-time-unit="0.013888889"/> </scenario> </experiment>
The results are not so different from our spreadsheet calculation. Again I want to add that this example is not complicated enough to demonstrate the full power of simulation over spreadsheet calculation for this use case.
One word to the statistical distributions: I used the Erlang distribution for the time between start events and normal distributions for processing times. I will not got into detail here, but see it as a good default. In your simulation projects you may want to use different distributions, which is not a problem, because the jBPM simulation supports more than these two.
You should take into account, that we ignored a warmup period in our experiment, so the system is underutilized at the beginning. The costs for the normal and worst case scenario are not very different, becauseI set the “unutilized-time-cost-factor” to zero, this means, the cost of unused resources is zero * resource-cost. So this is not included in the cost calculation. This can make sense, if the people can do other work in that time, normally the factor should be higher (at least because of set-up times for differnt types of work).
Here you can look at the reports, generated during the simulation run. First the comparison:
Or details on special scenarios:
- The raw desmo-j report for scenario “status_quo_normal”
- The graphical jbpm simulation report for scenario “status_quo_normal”
- The raw desmo-j report for scenario “christmas_normal”
- The graphical jbpm simulation report for scenario “christmas_normal”
Findind the best staffing strategy for christmas
Simulation cannot out-of-the-box calculate the right staffing strategy for you, but what it can do is to test your guesses. This means you have two possibilities: First, you can guess how many people you need and simulate with this number. If the resulting cycle times or utilization figures are not satisfying you try slightly different staffing scenarios till you get a result you can show to your boss. The second option is to automatically generate scenarios and chose the best result. This can be done by a brute force approach or some more sophisticated algorithm, genetic ones for instance. The important thing is, that you need to define a goal, which can be checked automatically, for example: “The average process cycle time must be better than 40 minutes, the slowest process must be finished after at least 3 hours”.
Let’s look at the first approach, where the simulation is only a tool to support your decision. I defined different staffing strategies as scenarios. You can see it as a guess from me.
<?xml version="1.0" encoding="UTF-8"?> <!-- experiment definition for jbpm simulation tutorial written by Bernd Ruecker (camunda GmbH) --> <experiment name='ReturnDefectiveGoods' time-unit='second' run-time='28800' real-start-time='30.03.1980 00:00:00:000' currency='€' unutilized-time-cost-factor='0.5'> <!-- 28800 seconds = 8 hours = 1 working day --> <!-- "abstract" simulation scenario base class --> <scenario name="christmas" execute="false"> <distribution name="ReturnDefectiveGoods.start" sample-type="real" type="erlang" mean="55"/> <distribution name="ReturnDefectiveGoods.wait for parcel" sample-type="real" type="normal" mean="28" standardDeviation="17"/> <distribution name="ReturnDefectiveGoods.transfer shipping costs" sample-type="real" type="normal" mean="180" standardDeviation="30"/> <distribution name="ReturnDefectiveGoods.quick test" sample-type="real" type="normal" mean="180" standardDeviation="60"/> <distribution name="ReturnDefectiveGoods.extended technical test" sample-type="real" type="normal" mean="732.2485" standardDeviation="448.1038"/> <distribution name="ReturnDefectiveGoods.send back goods" sample-type="real" type="normal" mean="325.5" standardDeviation="182.0718"/> <distribution name="ReturnDefectiveGoods.refund" sample-type="real" type="normal" mean="180" standardDeviation="30"/> <sim-process path="/ReturnDefectiveGoods/processdefinition.xml"> <process-overwrite start-distribution="ReturnDefectiveGoods.start"/> <state-overwrite state-name="wait for parcel" time-distribution="ReturnDefectiveGoods.wait for parcel"> <transition name="parcel arrived" probability="195"/> </state-overwrite> <decision-overwrite decision-name="ordered within the last two weeks?"> <transition name="YES" probability="100"/> <transition name="NO" probability="95"/> </decision-overwrite> <decision-overwrite decision-name="ordered within the last two weeks?"> <transition name="YES" probability="100"/> <transition name="NO" probability="95"/> </decision-overwrite> <task-overwrite task-name="transfer shipping costs" time-distribution="ReturnDefectiveGoods.transfer shipping costs"> <transition name="done" probability="100"/> </task-overwrite> <task-overwrite task-name="quick test" time-distribution="ReturnDefectiveGoods.quick test"> <transition name="no defect" probability="165"/> <transition name="defect approved" probability="30"/> </task-overwrite> <task-overwrite task-name="extended technical test" time-distribution="ReturnDefectiveGoods.extended technical test"> <transition name="no defect" probability="20"/> <transition name="defect approved" probability="145"/> </task-overwrite> <task-overwrite task-name="send back goods" time-distribution="ReturnDefectiveGoods.send back goods"> <transition probability="20"/> </task-overwrite> <task-overwrite task-name="refund" time-distribution="ReturnDefectiveGoods.refund"> <transition probability="175"/> </task-overwrite> </sim-process> </scenario> <scenario name="calculated_normal_case" execute="true" base-scenario="christmas"> <resource-pool name="accountant" pool-size="5" costs-per-time-unit="0.022222222"/> <resource-pool name="clerk" pool-size="4" costs-per-time-unit="0.011111111"/> <resource-pool name="tester" pool-size="11" costs-per-time-unit="0.025"/> <resource-pool name="dispatcher" pool-size="1" costs-per-time-unit="0.013888889"/> </scenario> <scenario name="calculated_worst_case" execute="true" base-scenario="christmas"> <resource-pool name="accountant" pool-size="6" costs-per-time-unit="0.022222222"/> <resource-pool name="clerk" pool-size="5" costs-per-time-unit="0.011111111"/> <resource-pool name="tester" pool-size="18" costs-per-time-unit="0.025"/> <resource-pool name="dispatcher" pool-size="1" costs-per-time-unit="0.013888889"/> </scenario> <scenario name="christmas_staff_1" execute="true" base-scenario="christmas"> <resource-pool name="accountant" pool-size="5" costs-per-time-unit="0.022222222"/> <resource-pool name="clerk" pool-size="5" costs-per-time-unit="0.011111111"/> <resource-pool name="tester" pool-size="13" costs-per-time-unit="0.025"/> <resource-pool name="dispatcher" pool-size="2" costs-per-time-unit="0.013888889"/> </scenario> <scenario name="christmas_staff_2" execute="true" base-scenario="christmas"> <resource-pool name="accountant" pool-size="5" costs-per-time-unit="0.022222222"/> <resource-pool name="clerk" pool-size="4" costs-per-time-unit="0.011111111"/> <resource-pool name="tester" pool-size="15" costs-per-time-unit="0.025"/> <resource-pool name="dispatcher" pool-size="2" costs-per-time-unit="0.013888889"/> </scenario> <scenario name="christmas_staff_3" execute="true" base-scenario="christmas"> <resource-pool name="accountant" pool-size="5" costs-per-time-unit="0.022222222"/> <resource-pool name="clerk" pool-size="4" costs-per-time-unit="0.011111111"/> <resource-pool name="tester" pool-size="17" costs-per-time-unit="0.025"/> <resource-pool name="dispatcher" pool-size="2" costs-per-time-unit="0.013888889"/> </scenario> <scenario name="christmas_staff_4" execute="true" base-scenario="christmas"> <resource-pool name="accountant" pool-size="6" costs-per-time-unit="0.022222222"/> <resource-pool name="clerk" pool-size="4" costs-per-time-unit="0.011111111"/> <resource-pool name="tester" pool-size="17" costs-per-time-unit="0.025"/> <resource-pool name="dispatcher" pool-size="1" costs-per-time-unit="0.013888889"/> </scenario> <scenario name="christmas_staff_5" execute="true" base-scenario="christmas"> <resource-pool name="accountant" pool-size="5" costs-per-time-unit="0.022222222"/> <resource-pool name="clerk" pool-size="5" costs-per-time-unit="0.011111111"/> <resource-pool name="tester" pool-size="17" costs-per-time-unit="0.025"/> <resource-pool name="dispatcher" pool-size="2" costs-per-time-unit="0.013888889"/> </scenario> <scenario name="christmas_staff_6" execute="true" base-scenario="christmas"> <resource-pool name="accountant" pool-size="6" costs-per-time-unit="0.022222222"/> <resource-pool name="clerk" pool-size="5" costs-per-time-unit="0.011111111"/> <resource-pool name="tester" pool-size="17" costs-per-time-unit="0.025"/> <resource-pool name="dispatcher" pool-size="2" costs-per-time-unit="0.013888889"/> </scenario> <scenario name="christmas_staff_7" execute="true" base-scenario="christmas"> <resource-pool name="accountant" pool-size="7" costs-per-time-unit="0.022222222"/> <resource-pool name="clerk" pool-size="6" costs-per-time-unit="0.011111111"/> <resource-pool name="tester" pool-size="20" costs-per-time-unit="0.025"/> <resource-pool name="dispatcher" pool-size="2" costs-per-time-unit="0.013888889"/> </scenario> <scenario name="christmas_staff_8" execute="true" base-scenario="christmas"> <resource-pool name="accountant" pool-size="8" costs-per-time-unit="0.022222222"/> <resource-pool name="clerk" pool-size="7" costs-per-time-unit="0.011111111"/> <resource-pool name="tester" pool-size="22" costs-per-time-unit="0.025"/> <resource-pool name="dispatcher" pool-size="3" costs-per-time-unit="0.013888889"/> </scenario>
And here the resulting overview table (you can download the complete graphical report here or the raw desmo-j reports here):
I don’t want to get into too much detail now. You can explore the results by yourself or play around with different parameters. You can easily analyze what influence your staffing strategy has on cycle time for different processes (if you include different processes in the scenarios, all would be listed in the table above), the costs, the average resource utilization and the bottleneck (which means, the resource with the biggest waiting times. However, this is not always a serious bottleneck!). Please note: In this example I have told the tool to include the half of the underutilized time of our stuff as costs.
Normally you would now do that simulation game in some iterations, testing different hypotheses and move closer to your goal. By doing that, you gather a better understanding of the dynamics of your system, so you are able to make a well founded decision. In real life you may also want to repeat the simulation runs several times with different seeds (start value for random numbers) to get a better result. The more runs you make, or the longer the single runs are, the better the results.
Even if that looks rather simple now, and actually it is for easy cases, you should be a bit aware: Applying simulation for complicated real life situations needs some experience.
Using algorithm to generate scenarios
The second approach would be to automatically find the best solution. Because generating solution candidates depends on your special application, I couldn’t implement a general solution which ships with the simulation tool of jBPM. For the problem described in this tutorial, the solution could be to overwrite the ExperimentReader, so every time it reads a scenario, it adds multiple scenarios to the experiment by just automatically provide different staffing strategies. To demonstrate the idea, I provide an example in the tutorial source, the StaffingExperimentReader, which uses a very easy algorithm to generate staffing scenarios. In reality you should think about better ways to do that.
public class StaffingExperimentReader extends ExperimentReader { private long addCount = 2; /** * flag to avoid endless loop */ private boolean afterScenarioReadActive = false; public StaffingExperimentReader(InputSource inputSource) { super(inputSource); } public StaffingExperimentReader(String experimentXml) { super(experimentXml); } protected void afterScenarioRead(JbpmSimulationScenario scenario, Element scenarioElement, Element baseScenarioElement) { if (afterScenarioReadActive) return; afterScenarioReadActive = true; HashMap pools = new HashMap(); HashMap costs = new HashMap(); Iterator poolElementIter = scenarioElement.elementIterator("resource-pool"); while (poolElementIter.hasNext()) { Element resourcePoolElement = (Element) poolElementIter.next(); String poolName = resourcePoolElement.attributeValue("name"); String poolSizeText = resourcePoolElement.attributeValue("pool-size"); Integer poolSize = new Integer(poolSizeText); pools.put(poolName, poolSize); costs.put(poolName, readCostPerTimeUnit(resourcePoolElement)); } // add more scenarios with more people // (so the provided people count is the lower limit) for (int add = 1; add <= addCount; add++) { JbpmSimulationScenario generatedScenario = readScenario(scenarioElement, baseScenarioElement); generatedScenario.changeName(scenario.getName() + "+" + add); for (Iterator iterator = pools.keySet().iterator(); iterator.hasNext();) { String name = (String) iterator.next(); Integer size = (Integer) pools.get(name); size = new Integer(size.intValue() + add); generatedScenario.addResourcePool(name, size, (Double)costs.get(name)); } addScenario(generatedScenario); } afterScenarioReadActive = false; } protected void beforeScenarioRead(JbpmSimulationScenario scenario, Element scenarioElement, Element baseScenarioElement) { } }
This results in more scenarios, if you are interessted, here the graphical report from this simulation run.
Use Case 2: Evaluate process alternatives
Maybe your company wants to save money, which is indeed hard to image but maybe possible. So your manager has the idea that the technical tests are too expensive. He spoke to a third party company which would buy all returned goods, even untested. This would allow your company to skip these tests. Because you don’t want your customers to know, that they can return even working goods as defect, you make random checks of incoming goods.
Based on the figures given above, 15 % of goods are recognized as defect in the quick test, and 88 % of the remaining 85 % are proven as defect in the extended test. Based on these figures, 89,8 % of the returned goods are really defect. To be on the safe side and to pay their own handling, the hird party company charges you 90 % of the estimated resale value of all returned goods.
You now want to check using simulation if this scenario is cheaper for your company or not. First you model the new process:
After modeling, you create a new scenario reflecting the changes. This scenario can be compared to the original one (status quo) later to decide which one is cheaper. The scenario is mostly the same, so I will only stress the differences now. Since the “unutilized-time-cost-factor” is set to zero,not utilized resources cause no costs, so I didn’t change the staffing strategy for simplicit, normally you would obviously try to reduce required people too.
The scenario configuration looks like this:
<scenario name="alternative" execute="true"> <business-figure name = "value reduction of returned goods" type = "costs" automatic-calculation = "none" handler = "org.jbpm.sim.tutorial.TutorialBusinessFigureCalculator" /> <data-source name="return orders" handler="org.jbpm.sim.tutorial.TutorialDataSource" /> ...[distributions]... <sim-process path="/ReturnDefectiveGoodsAlternative/processdefinition.xml"> <process-overwrite start-distribution="ReturnDefectiveGoods.start"> <use-data-source name="return orders" /> </process-overwrite> ...[overwrites like in status quo, without non existing "quick test"]... <!-- lets assume 5 % of the orders are checked --> <decision-overwrite decision-name="should be checked?"> <transition name="check" probability="5"/> <transition name="no check" probability="95"/> </decision-overwrite> <!-- and calculate "virtual business costs" --> <node-overwrite node-name='unknown goods status'> <calculate-business-figure name='value reduction of returned goods' /> </node-overwrite> </sim-process> ...[resources]... </scenario>
Too make the scnearios comparable the status quo also has to be changed to include a business figure, which adds the resale value of proven defect goods as costs:
<scenario name="status_quo" execute="true"> <business-figure name = "defect goods" type = "costs" automatic-calculation = "none" handler = "org.jbpm.sim.tutorial.DefectGoodsCostsCalculator" /> <data-source name="return orders" handler="org.jbpm.sim.tutorial.TutorialDataSource" /> ... <sim-process path="/ReturnDefectiveGoods/processdefinition.xml"> <process-overwrite start-distribution="ReturnDefectiveGoods.start"> <use-data-source name="return orders" /> </process-overwrite> ... <task-overwrite task-name="refund" time-distribution="ReturnDefectiveGoods.refund"> <calculate-business-figure name='defect goods' /> <transition probability="175"/> </task-overwrite> ... </sim-process> </scenario>
Data-Source
The data-source is straightforward, it just create return order objects and add them to the started process. We need these process variables later to know, how much is the estimated resale value of the returned goods. Here the code of the data-source:
public class TutorialDataSource implements ProcessDataSource { ... public void addNextData(ExecutionContext ctx) { ctx.getContextInstance().createVariable("returnOrder", getProcessVariable()); next(); } ... private Object getProcessVariable() { switch (state) { case 0: return new ReturnOrder(getDate(10), 100.0, 75.0); case 1: return new ReturnOrder(getDate(10), 100.0, 75.0); ... } return null; } public boolean hasNext() { return true; } }
Business Figure
The business figure is responsible to add costs to this process instance, in the case it is not checked. The calculator is therefor hooked into the process and just has to return a number for the calculated costs. The code is again straighforward:
public class TutorialBusinessFigureCalculator implements BusinessFigureCalculator { public Number calculate(ExecutionContext executionContext) { ReturnOrder o = (ReturnOrder)executionContext. getContextInstance().getVariable("returnOrder"); double resaleValue = o.getEstimatedResaleValue(); // "virtual costs" of 90 % of the value of the goods return Double.valueOf( resaleValue * 0.90 ); } }
Result
Now we can run the simulation. Because the whole tutorial is available for you to play around with yourself, I will just concentrate on the important figure, the costs:
- Costs for the status quo (as already known from the above simulation): 19.516 €
- Costs for the alternative solution: 16.401 €
This means, the new process version would really safe money! Normally you should do multiple simulation runs now to prove these results. Also some changes in staffing scenarios or maybe evaluating a good percentage of checked goods would be a good starting point. Also it may depend very much on the goods you handle, currently all the historical data and the data source is not very realistic. And normally the company gets a refund for defect goods from the manufacturer, if it is broken within the warranty for example.
But anyway, isn’t that better than just discuss on a power point level if the outsourcing makes sense?
Open Issues & further development
There are still open issues in the area of the jbpm simulation tool. Also I am not sure, if and how some of the features will be used, because the current project where we apply the simulation uses only a subset of the provided features.
I want to mention some open issues explicitly:
- Make historical data discovery database independent
- Add units to measures in reports, add possibility to use other unit in reports (for example minutes/hours in process cycle times)
And there are still basic open issues for the whole simulation tool (this list is not complete):
- Think about efficient strategy to queue up if different resources are needed (and avoid deadlocks)
- Introduce sophisticated shift calendar for resources
- Think about timers & jobs
Same holds true for the graphical reports. I created them to show how you can build reports and to setup a working environment with working examples. Feel free to use Jasper IReport to design your own reports fitting to your special problem.
I am very interessting in how you use the described simulation environment and would be happy about a short mail, how you use it, if it was useful, what you missed and also general experiences! My Email adress is: bernd.ruecker@camunda.com , don’t hestitate to contact me.