Preparing the OEE Simulation

For the OEE Simulation, a sample enterprise needs to be created in the ISA-95 Equipment Model and in the I/O Model sample production data needs to be generated.

Creating the Sample Enerprise in the ISA-95 Equipment Model

Go to the ISA-95 Equipment Model panel, then open a Console. Copy & paste the following script to the console, and run it. This script prepares the demo Enterprise in the ISA-95 Equipment Model, which then should look similar to this:

The Sample Enterprise in the ISA-95 Model
Figure 1. The Sample Enterprise in the ISA-95 Model

Alternatively you can create this structure by hand.

Preparation of the ISA-95 Model
local base = "/"

syslib.mass({
	{
		class = syslib.model.classes.S95EMEnterprise,
		operation = syslib.model.codes.MassOp.UPSERT,
		path =  base .. "The OEE Demo Enterprise",
		["ID"] = "The OEE Demo Enterprise",
		["Description"] = "The sample enterprise for OEE Simulation",
		["Criticality"] = 0
	},
	{
		class = syslib.model.classes.S95EMSite,
		operation = syslib.model.codes.MassOp.UPSERT,
		path =  base .. "The OEE Demo Enterprise/The OEE Demo Site",
		["ID"] = "The OEE Demo Site",
		["Description"] = "a sample site for OEE simulation",
		["Criticality"] = 0
	},
	{
		class = syslib.model.classes.S95EMArea,
		operation = syslib.model.codes.MassOp.UPSERT,
		path =  base .. "The OEE Demo Enterprise/The OEE Demo Site/The OEE Demo Area",
		["ID"] = "The OEE Demo Area",
		["Description"] = "a sample area for OEE simulation",
		["Criticality"] = 0
	},
	{
		class = syslib.model.classes.S95EMProcessCell,
		operation = syslib.model.codes.MassOp.UPSERT,
		path =  base .. "The OEE Demo Enterprise/The OEE Demo Site/The OEE Demo Area/The OEE Demo Cell",
		["ID"] = "The OEE Demo Cell",
		["Description"] = "the cell monitored for OEE",
		["Criticality"] = 0
	}
})

OEE Simulation Data Setup

In the I/O Model, create an Action Item object, calling it 'OEE Simulator'.

Copy the source code of the OEE Simulation Script (below) into the Action Item’s Lua Script Body and enable Dedicated Thread Execution property for the Action Item.

Adding the simulator script to a new Action Item
Figure 2. Adding the simulator script to a new Action Item

When you click Create, the script will generate a number of Variable objects. The script then produces simulation data (which is stored in those variables) for the OEE calculation.

The Variable objects generated by the OEE Simulator script
Figure 3. The Variable objects generated by the OEE Simulator script

The OEE Simulation Script

OEE Simulation Script
-- !! Please put the content of this file to an action item and enable dedicated thread execution !!

-- CONFIGURATION
local Timebasis = 1000                -- ms, Timebasis of the simulation
local MaxCycle = 20                   -- Max number of cycles
local Product = "Discrete Product A"            -- Product name
local FailureChance = 10              -- %, Chance of a failure to happen
local FailureMaxDuration = 2          -- How many cycles failure state should last
local PerformanceDropChance = 0       -- %, Chance of a performance drop to happen
local LowestPerfromanceDropLimit = 50 -- %, Performance drop will be calcualted as a random value within [LowestPerfromanceDropLimit%, 100%]
local ScrapChance = 20                -- %, Chance of scrap product to happen
local HighestScrapLimit = 50          -- %, Scrap amount will be calcualted as a random value within [0%, HighestScrapLimit%]

-- PRODUCT DATABASE
-- this database has the data related to a specific produc
local products = {
	["Discrete Product A"] = {
		performance = 10,	              	-- producing 10 units per specified timebasis
		failurecode = 1,		              -- this failure code will be used in case of an error
		digits = 0,
		batchpattern = "Discrete Product Batch %02d",
	},
	["Discrete Product B"] = {
		performance = 10,
		failurecode = 2,
		digits = 0,
		batchpattern = "Discrete Product [10] Batch %02d",
	},
	["Discrete Product C"] = {
		performance = 20,
		failurecode = 3,
		digits = 0,
		batchpattern = "Discrete Product [20] Batch %02d",
	},
	["Liquid Product A"] = {
		performance = 2.5,
		failurecode = 4,
		digits = 2,
		batchpattern = "Liquid Product Batch %02d",
	},
	["Liquid Product B"] = {
		performance = 2.5,
		failurecode = 5,
		digits = 2,
		batchpattern = "Liquid Product [2.5] Batch %02d",
	},
	["Liquid Product C"] = {
		performance = 3.5,
		failurecode = 6,
		digits = 2,
		batchpattern = "Liquid Product [3.5] Batch %02d",
	}
}

-- HELPER FUNCTIONS
local var = require"esi-variables" -- helper module to deal with variables creatings and writing values into those
local round = function(num, dec) -- helper funciton math.round
	local mult = 10 ^ (dec or 0)
	return math.floor(num * mult + 0.5) / mult
end

-- SIMULATION
-- this data will hekp us to determine the current state of the simulation,
-- and to understand if we need to continue the simulation, or stop it
local self = syslib.getself()     -- get self object
local cfg_ver = self:cfgversion() -- get current cfg version of self

-- get some data from the product database
local perf = ((products or {})[Product] or {}).performance or 1	-- default performance
local failcode = ((products or {})[Product] or {}).failurecode or 1 -- failure code to use in case of a failure
local digits = ((products or {})[Product] or {}).digits or 0	-- for the math.round function
local pattern = ((products or {})[Product] or {}).batchpattern or "Batch %s" -- specific pattern for the batch id

-- helper variables which holds current state of the simulation
local cycle  = -2 -- cycle counter, a single cycle is equal to the timebasis
local failcycle = 0 -- fail cycles counter, this one to make the length of failures random
local batchcount = 0 -- the number of the current batch
local currincr = 0 -- internal quantity related to the current cycle
local quantity = 0 -- total quantity output (total), incrementing
local scrap = 0 -- total scrap output (total), incrementin
local eqstate = syslib.model.codes.EquipmentStates.ABORTED -- actual equipment state
local errcode = nil -- actual error coed

-- check if we should continue the simulation
while self:enabled() and cfg_ver == self:cfgversion() do

	-- set some output to the action item
	syslib.setvalue(self, ("[%d] Simulation is running..."):format(cycle))

	-- cycle < 0 indicates that simulation is being resetting.
    if cycle < 0 then
		-- reset all important variables
        batchcount = 0
        quantity = 0
        scrap = 0
		-- set state to ABORTED (so equipment monitor can catch it as a 'reset event')
        eqstate = syslib.model.codes.EquipmentStates.ABORTED
    end

	-- cycle 0 means we have started a new batch
    if cycle == 0 then
		-- increasing the batch counter
        batchcount = batchcount + 1
		-- setting quantity and scrap to 0
        quantity = 0
        scrap = 0
		-- equipment state STARTING, so equipment monitor can catch the 'start event'
        eqstate = syslib.model.codes.EquipmentStates.STARTING
    end

	-- until we reached our max cycle value, we do simulation
    if cycle >= 1 and cycle <= MaxCycle then
		-- check if we need to go into a failure route
        if failcycle == 0 and math.random(100) > FailureChance then -- NORMAL ROUTE
			-- running state
            eqstate = syslib.model.codes.EquipmentStates.EXECUTE_RUNNING

			-- emulate performance degradation with some chance
            if math.random(100) > PerformanceDropChance then
				-- no degradation in performance, increment is our default performance
                currincr = perf
            else
				-- degradation took place, increment is less than our default performance
                currincr = round(perf * math.random(LowestPerfromanceDropLimit, 100) / 100, digits)
            end
			-- adding incremented value to the total quantity
			quantity = quantity + currincr

			-- emulate the chance of scrap output
            if not (math.random(100) > ScrapChance) then
                scrap = scrap + round(currincr * math.random(0, HighestScrapLimit) / 100, digits)
            end

			-- error is nil, we are in the normal route
            errcode = nil

        else -- FAILURE ROUTE

			-- if we stepped in by the failure chance condition, we should decide how long failure should last
			if failcycle == 0 then failcycle = math.random(1, FailureMaxDuration) end

			-- equipment state is STOPPED
            eqstate = syslib.model.codes.EquipmentStates.STOPPED

			-- producing a failure event
            syslib.setevent({ Message = "An Error Occured" })

			-- use our reserved vailure code
            errcode = failcode

			-- fail cycle count is decreasing, when it reaches 0 we will go back to normal operation
			failcycle = failcycle - 1
        end
    end

	-- when we reached max cycle value, we need to indicate the end of the current batch
	if cycle == MaxCycle then
		-- cycle to -1, so it will be set to 0 later
		cycle = -1
		-- resolve error to safely close the current batch
		errcode = nil
		failcycle = 0
		-- state COMPLETE, so equipment monitor can catch the 'end event'
		eqstate = syslib.model.codes.EquipmentStates.COMPLETE
	end

	-- create variable (if they are not yet created) and write the current values to those.
	-- this all will be carried by esi-variable library, hist = true - means enable archiving
	var:SET({path = "Product", v = Product, hist = true})
	var:SET({path = "Batch ID", v = (pattern):format(batchcount), hist = true})
    var:SET({path = "QuantityTotal", v = quantity, hist = true})
	var:SET({path = "QuantityGood", v = quantity - scrap, hist = true})
    var:SET({path = "QuantityBad", v = scrap, hist = true})
    var:SET({path = "Error Code", v = errcode, hist = true})
	var:SET({path = "Equipment State", v = eqstate, hist = true})

	-- incrementing the cycle counter variable
    cycle = cycle + 1

	-- some delay to reflect the timebasis
	-- this logic had been slightly improved to make the simulation aborting process more flexible.

	local now = syslib.now()
	-- untill this time we should delay our code
	local untill = now - now % 1000 + Timebasis  -- some simple time syncronization
	local singlewait = 100
	while syslib.now() < untill do
		now = syslib.now()
		if not (self:enabled() and cfg_ver == self:cfgversion()) then break end
		if untill - now < singlewait then singlewait = untill - now end
		if singlewait < 10 then break end
		syslib.sleep(singlewait)
	end
end

Configuration Options

The Configuration section of the script has a number of values which affect the simulated production and which can be changed. Product defines for which entry of the Product Database (see below) the OEE indices will be calculated.

The Product Database section of the script provides a number of sample products. Feel free to add new products, following the example of the existing entries.