<reponame>SoftwareAG/apama-eplapps-tools
# Copyright (c) 2021 Software AG, Darmstadt, Germany and/or its licensors

# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
# file except in compliance with the License. You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under the
# License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
# either express or implied.
# See the License for the specific language governing permissions and limitations under the License.

import random, math
from apamax.eplapplications.perf import ObjectCreator

# The interval (in seconds) at which measurements for occupancy count of a building are generated. See the 
# OUTPUT_GENERATION_FREQUENCY constant in the BuildingOccupancyCalculation EPL file.
OCCUPANCY_COUNT_MEASUREMENT_INTERVAL = 1.0
# The number of alarms to generate before generating an operation.
ALARMS_PER_OPERATION = 20
# Max rate of total alarms generated by all buildings. The actual alarm rate would be usually a lot less than this.
MAX_TOTAL_ALARMS_RATE = 10
class MeasurementCreator(ObjectCreator):
	"""
		Create simulated measurements for all access points in a building. One instance 
		of this class is responsible for one building.

		Produce values in a cyclic zip-zap pattern. One cycle creates one alarm.
		The values are generated such that the total occupancy of the building becomes equal 
		to a calculated max value with a calculated rate. This max value is calculated from 
		the occupancy threshold value such that once occupancy count crosses the threshold 
		value, it stays above the threshold value for some time so that the total occupancy count 
		measurement is generated (once a second) and an alarm is generated (and an operation 
		once every few alarms).

		Once occupancy count reaches the max value, it gets decremented with the same rate 
		until it becomes zero. This cycle is repeated until the end of the test.

		The max value required to generate an operation is significantly higher than the max 
		value required for an alarm because we need to generate an alarm and hold it without 
		clearing it for some time to generate an operation.

	"""

	def __init__(self, measurementType, fragment, enteredSeries, exitedSeries,
				 numOfBuildings, numOfAccessPointsPerBuilding, measurementRatePerAccessPoint, 
				 occupancyThreshold, operationThresholdDurationMin):
		super(MeasurementCreator, self).__init__()

		# Parameters for measurement shape - unchanged
		self.MEASUREMENT_TYPE = measurementType
		self.FRAGMENT = fragment
		self.SERIES_ENTERED = enteredSeries
		self.SERIES_EXITED = exitedSeries

		# Options to generate measurement values, unchanged once configured.
		self.AP_VAL_CHANGE = None       # Increase or decrease in occupancy from a single access point per measurement.
		self.MAX_VAL_ALARMS_GEN = None  # Maximum occupancy to achieve for guaranteed generation of an alarm.
		self.MAX_VAL_OPS_GEN = None     # Maximum occupancy to achieve for guaranteed generation of an operation.
		# Configure options to generate measurement values
		self.configure(numOfBuildings, numOfAccessPointsPerBuilding, measurementRatePerAccessPoint,
			occupancyThreshold, operationThresholdDurationMin)

		# Current internal state
		self.increaseOccupancy = True  # Should the total occupancy of the building be increased?
		self.currentOccupancy = 0      # Current building occupancy.
		self.cycleCount = 1            # Current cycle count
		self.accessPointCounts = {}    # Current total entered and exited counts of all access points of the building.

	def configure(self, numOfBuildings, numOfAccessPointsPerBuilding, measurementRatePerAccessPoint,
			occupancyThreshold, operationThresholdDurationMin):
		
		# Alarm rate per building
		ALARM_RATE_PER_BUILDING = float(MAX_TOTAL_ALARMS_RATE) / numOfBuildings

		# For an alarm generation, the occupancy of the building should be kept above the occupancy
		# threshold for at least 1 second so that total measurement with the total occupancy count 
		# is generated which results in an alarm.
		durationAboveThresholdForAlarm = OCCUPANCY_COUNT_MEASUREMENT_INTERVAL + 1  # 1 sec margin
		
		# For an operation generation, the occupancy of the building should be kept above the occupancy 
		# threshold until an alarm is generated and is not cleared for enough time so that an operation is generated.
		durationAboveThresholdForOperation = durationAboveThresholdForAlarm + operationThresholdDurationMin * 60 + 5  # Keep a 5s margin

		# Find the time to reach the threshold to achieve the target alarm rate.
		# timeBetweenAlarms = cyclePeriod = timeToBreachThreshold + durationAboveThresholdForAlarm + timeToBreachThreshold = 1 / ALARM_RATE_PER_BUILDING
		# 2*timeToBreachThreshold + durationAboveThresholdForAlarm = 1 / ALARM_RATE_PER_BUILDING
		timeToBreachThreshold = (1 / ALARM_RATE_PER_BUILDING - durationAboveThresholdForAlarm) / 2

		# If the target alarm rate is not achievable (time to create an alarm is less than time required
		# to stay abve the threshold value for alarm generation) or the time required to 
		# breach the threshold is less than the time to publish at least 5 measurements
		# for an access point then increase the timeToBreachThreshold to reduce the alarm rate.
		if timeToBreachThreshold < 0 or timeToBreachThreshold * measurementRatePerAccessPoint < 5.0:
			timeToBreachThreshold = max(5.0 / measurementRatePerAccessPoint, 1.0)

		# Change in occupancy per measurement from an access point to breach the occupancyThreshold in timeToBreachThreshold seconds.
		self.AP_VAL_CHANGE = math.ceil(occupancyThreshold / (numOfAccessPointsPerBuilding * measurementRatePerAccessPoint * timeToBreachThreshold))
		
		# The rate at which a building's occupancy changes.
		changeRatePerBuilding = numOfAccessPointsPerBuilding * measurementRatePerAccessPoint * self.AP_VAL_CHANGE
		
		# Maximum occupancy value to reach for guaranteed generation an alarm.
		self.MAX_VAL_ALARMS_GEN =  (timeToBreachThreshold + durationAboveThresholdForAlarm / 2) * changeRatePerBuilding
		
		# Maximum occupancy value to reach for guaranteed generation an operation.
		self.MAX_VAL_OPS_GEN = (timeToBreachThreshold + durationAboveThresholdForOperation / 2) * changeRatePerBuilding

	def createObject(self, device, sendTime):
		"""
		Create a new measurement value.

		Generate a value that increases or decreases the total occupancy count of the building by `self.AP_VAL_CHANGE`.
		Reverse the direction of the change for the next value, if reached to the top or bottom.
		"""
		
		# Change in the occupancy count of the building
		if self.increaseOccupancy:
			totalChange = self.AP_VAL_CHANGE
		else:
			# decrease the value
			totalChange = -self.AP_VAL_CHANGE
			# If the total is going to be less than zero, update the 'totalChange' so that it becomes zero.
			if self.currentOccupancy + totalChange < 0:
				totalChange = -self.currentOccupancy

		# Update the total occupancy in the building
		self.currentOccupancy = self.currentOccupancy + totalChange

		# Split the change in the occupancy into entered and exited count so that the total 
		# change in the occupancy is still equal to the `totalChange`. `entered - exited == totalChange`
		random_val = random.randint(0, self.AP_VAL_CHANGE)	# use random value for splitting
		(entered, exited) = self.accessPointCounts.get(device, (0, 0))  # (number of people entered, number of people exited)
		if totalChange > 0:
			entered = entered + totalChange + random_val
			exited = exited + random_val
		else:
			exited = exited - totalChange + random_val
			entered = entered + random_val

		self.accessPointCounts[device] = (entered, exited)

		# create measurement to send
		measurement = {
			'time': sendTime,
			"type": self.MEASUREMENT_TYPE,
			"source": {
				"id": device
			},
			self.FRAGMENT: {
				self.SERIES_ENTERED: {
					"value": entered
				},
				self.SERIES_EXITED: {
					"value": exited
				}
			}
		}

		# Check if reached top or bottom
		if self.currentOccupancy <= 0:
			self.currentOccupancy = 0
			# Increase the cycle count
			self.cycleCount = self.cycleCount + 1
			# The occupancy count should go up now.
			self.increaseOccupancy = True
		elif self.currentOccupancy >= self.MAX_VAL_OPS_GEN:
			# Start decreasing the occupancy count
			self.increaseOccupancy = False
		elif self.currentOccupancy >= self.MAX_VAL_ALARMS_GEN:
			# Start decreasing the occupancy count only if we are not in the
			# cycle that creates an operation
			if self.cycleCount % ALARMS_PER_OPERATION != 0:
				self.increaseOccupancy = False
			else:
				self.increaseOccupancy = True

		return measurement
		

# Copyright (c) 2021 Software AG, Darmstadt, Germany and/or its licensors

# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
# file except in compliance with the License. You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under the
# License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
# either express or implied.
# See the License for the specific language governing permissions and limitations under the License.

import random, math
from apamax.eplapplications.perf import ObjectCreator

# The interval (in seconds) at which measurements for occupancy count of a building are generated. See the 
# OUTPUT_GENERATION_FREQUENCY constant in the BuildingOccupancyCalculation EPL file.
OCCUPANCY_COUNT_MEASUREMENT_INTERVAL = 1.0
# The number of alarms to generate before generating an operation.
ALARMS_PER_OPERATION = 20
# Max rate of total alarms generated by all buildings. The actual alarm rate would be usually a lot less than this.
MAX_TOTAL_ALARMS_RATE = 10
class MeasurementCreator(ObjectCreator):
	"""
		Create simulated measurements for all access points in a building. One instance 
		of this class is responsible for one building.

		Produce values in a cyclic zip-zap pattern. One cycle creates one alarm.
		The values are generated such that the total occupancy of the building becomes equal 
		to a calculated max value with a calculated rate. This max value is calculated from 
		the occupancy threshold value such that once occupancy count crosses the threshold 
		value, it stays above the threshold value for some time so that the total occupancy count 
		measurement is generated (once a second) and an alarm is generated (and an operation 
		once every few alarms).

		Once occupancy count reaches the max value, it gets decremented with the same rate 
		until it becomes zero. This cycle is repeated until the end of the test.

		The max value required to generate an operation is significantly higher than the max 
		value required for an alarm because we need to generate an alarm and hold it without 
		clearing it for some time to generate an operation.

	"""

	def __init__(self, measurementType, fragment, enteredSeries, exitedSeries,
				 numOfBuildings, numOfAccessPointsPerBuilding, measurementRatePerAccessPoint, 
				 occupancyThreshold, operationThresholdDurationMin):
		super(MeasurementCreator, self).__init__()

		# Parameters for measurement shape - unchanged
		self.MEASUREMENT_TYPE = measurementType
		self.FRAGMENT = fragment
		self.SERIES_ENTERED = enteredSeries
		self.SERIES_EXITED = exitedSeries

		# Options to generate measurement values, unchanged once configured.
		self.AP_VAL_CHANGE = None       # Increase or decrease in occupancy from a single access point per measurement.
		self.MAX_VAL_ALARMS_GEN = None  # Maximum occupancy to achieve for guaranteed generation of an alarm.
		self.MAX_VAL_OPS_GEN = None     # Maximum occupancy to achieve for guaranteed generation of an operation.
		# Configure options to generate measurement values
		self.configure(numOfBuildings, numOfAccessPointsPerBuilding, measurementRatePerAccessPoint,
			occupancyThreshold, operationThresholdDurationMin)

		# Current internal state
		self.increaseOccupancy = True  # Should the total occupancy of the building be increased?
		self.currentOccupancy = 0      # Current building occupancy.
		self.cycleCount = 1            # Current cycle count
		self.accessPointCounts = {}    # Current total entered and exited counts of all access points of the building.

	def configure(self, numOfBuildings, numOfAccessPointsPerBuilding, measurementRatePerAccessPoint,
			occupancyThreshold, operationThresholdDurationMin):
		
		# Alarm rate per building
		ALARM_RATE_PER_BUILDING = float(MAX_TOTAL_ALARMS_RATE) / numOfBuildings

		# For an alarm generation, the occupancy of the building should be kept above the occupancy
		# threshold for at least 1 second so that total measurement with the total occupancy count 
		# is generated which results in an alarm.
		durationAboveThresholdForAlarm = OCCUPANCY_COUNT_MEASUREMENT_INTERVAL + 1  # 1 sec margin
		
		# For an operation generation, the occupancy of the building should be kept above the occupancy 
		# threshold until an alarm is generated and is not cleared for enough time so that an operation is generated.
		durationAboveThresholdForOperation = durationAboveThresholdForAlarm + operationThresholdDurationMin * 60 + 5  # Keep a 5s margin

		# Find the time to reach the threshold to achieve the target alarm rate.
		# timeBetweenAlarms = cyclePeriod = timeToBreachThreshold + durationAboveThresholdForAlarm + timeToBreachThreshold = 1 / ALARM_RATE_PER_BUILDING
		# 2*timeToBreachThreshold + durationAboveThresholdForAlarm = 1 / ALARM_RATE_PER_BUILDING
		timeToBreachThreshold = (1 / ALARM_RATE_PER_BUILDING - durationAboveThresholdForAlarm) / 2

		# If the target alarm rate is not achievable (time to create an alarm is less than time required
		# to stay abve the threshold value for alarm generation) or the time required to 
		# breach the threshold is less than the time to publish at least 5 measurements
		# for an access point then increase the timeToBreachThreshold to reduce the alarm rate.
		if timeToBreachThreshold < 0 or timeToBreachThreshold * measurementRatePerAccessPoint < 5.0:
			timeToBreachThreshold = max(5.0 / measurementRatePerAccessPoint, 1.0)

		# Change in occupancy per measurement from an access point to breach the occupancyThreshold in timeToBreachThreshold seconds.
		self.AP_VAL_CHANGE = math.ceil(occupancyThreshold / (numOfAccessPointsPerBuilding * measurementRatePerAccessPoint * timeToBreachThreshold))
		
		# The rate at which a building's occupancy changes.
		changeRatePerBuilding = numOfAccessPointsPerBuilding * measurementRatePerAccessPoint * self.AP_VAL_CHANGE
		
		# Maximum occupancy value to reach for guaranteed generation an alarm.
		self.MAX_VAL_ALARMS_GEN =  (timeToBreachThreshold + durationAboveThresholdForAlarm / 2) * changeRatePerBuilding
		
		# Maximum occupancy value to reach for guaranteed generation an operation.
		self.MAX_VAL_OPS_GEN = (timeToBreachThreshold + durationAboveThresholdForOperation / 2) * changeRatePerBuilding

	def createObject(self, device, sendTime):
		"""
		Create a new measurement value.

		Generate a value that increases or decreases the total occupancy count of the building by `self.AP_VAL_CHANGE`.
		Reverse the direction of the change for the next value, if reached to the top or bottom.
		"""
		
		# Change in the occupancy count of the building
		if self.increaseOccupancy:
			totalChange = self.AP_VAL_CHANGE
		else:
			# decrease the value
			totalChange = -self.AP_VAL_CHANGE
			# If the total is going to be less than zero, update the 'totalChange' so that it becomes zero.
			if self.currentOccupancy + totalChange < 0:
				totalChange = -self.currentOccupancy

		# Update the total occupancy in the building
		self.currentOccupancy = self.currentOccupancy + totalChange

		# Split the change in the occupancy into entered and exited count so that the total 
		# change in the occupancy is still equal to the `totalChange`. `entered - exited == totalChange`
		random_val = random.randint(0, self.AP_VAL_CHANGE)	# use random value for splitting
		(entered, exited) = self.accessPointCounts.get(device, (0, 0))  # (number of people entered, number of people exited)
		if totalChange > 0:
			entered = entered + totalChange + random_val
			exited = exited + random_val
		else:
			exited = exited - totalChange + random_val
			entered = entered + random_val

		self.accessPointCounts[device] = (entered, exited)

		# create measurement to send
		measurement = {
			'time': sendTime,
			"type": self.MEASUREMENT_TYPE,
			"source": {
				"id": device
			},
			self.FRAGMENT: {
				self.SERIES_ENTERED: {
					"value": entered
				},
				self.SERIES_EXITED: {
					"value": exited
				}
			}
		}

		# Check if reached top or bottom
		if self.currentOccupancy <= 0:
			self.currentOccupancy = 0
			# Increase the cycle count
			self.cycleCount = self.cycleCount + 1
			# The occupancy count should go up now.
			self.increaseOccupancy = True
		elif self.currentOccupancy >= self.MAX_VAL_OPS_GEN:
			# Start decreasing the occupancy count
			self.increaseOccupancy = False
		elif self.currentOccupancy >= self.MAX_VAL_ALARMS_GEN:
			# Start decreasing the occupancy count only if we are not in the
			# cycle that creates an operation
			if self.cycleCount % ALARMS_PER_OPERATION != 0:
				self.increaseOccupancy = False
			else:
				self.increaseOccupancy = True

		return measurement
# Copyright (c) 2021 Software AG, Darmstadt, Germany and/or its licensors # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this # file except in compliance with the License. You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software distributed under the # License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, # either express or implied. # See the License for the specific language governing permissions and limitations under the License. # The interval (in seconds) at which measurements for occupancy count of a building are generated. See the # OUTPUT_GENERATION_FREQUENCY constant in the BuildingOccupancyCalculation EPL file. # The number of alarms to generate before generating an operation. # Max rate of total alarms generated by all buildings. The actual alarm rate would be usually a lot less than this. Create simulated measurements for all access points in a building. One instance  		of this class is responsible for one building.  		Produce values in a cyclic zip-zap pattern. One cycle creates one alarm. 		The values are generated such that the total occupancy of the building becomes equal  		to a calculated max value with a calculated rate. This max value is calculated from  		the occupancy threshold value such that once occupancy count crosses the threshold  		value, it stays above the threshold value for some time so that the total occupancy count  		measurement is generated (once a second) and an alarm is generated (and an operation  		once every few alarms).  		Once occupancy count reaches the max value, it gets decremented with the same rate  		until it becomes zero. This cycle is repeated until the end of the test.  		The max value required to generate an operation is significantly higher than the max  		value required for an alarm because we need to generate an alarm and hold it without  		clearing it for some time to generate an operation. # Parameters for measurement shape - unchanged # Options to generate measurement values, unchanged once configured. # Increase or decrease in occupancy from a single access point per measurement. # Maximum occupancy to achieve for guaranteed generation of an alarm. # Maximum occupancy to achieve for guaranteed generation of an operation. # Configure options to generate measurement values # Current internal state # Should the total occupancy of the building be increased? # Current building occupancy. # Current cycle count # Current total entered and exited counts of all access points of the building. # Alarm rate per building # For an alarm generation, the occupancy of the building should be kept above the occupancy # threshold for at least 1 second so that total measurement with the total occupancy count # is generated which results in an alarm. # 1 sec margin # For an operation generation, the occupancy of the building should be kept above the occupancy # threshold until an alarm is generated and is not cleared for enough time so that an operation is generated. # Keep a 5s margin # Find the time to reach the threshold to achieve the target alarm rate. # timeBetweenAlarms = cyclePeriod = timeToBreachThreshold + durationAboveThresholdForAlarm + timeToBreachThreshold = 1 / ALARM_RATE_PER_BUILDING # 2*timeToBreachThreshold + durationAboveThresholdForAlarm = 1 / ALARM_RATE_PER_BUILDING # If the target alarm rate is not achievable (time to create an alarm is less than time required # to stay abve the threshold value for alarm generation) or the time required to # breach the threshold is less than the time to publish at least 5 measurements # for an access point then increase the timeToBreachThreshold to reduce the alarm rate. # Change in occupancy per measurement from an access point to breach the occupancyThreshold in timeToBreachThreshold seconds. # The rate at which a building's occupancy changes. # Maximum occupancy value to reach for guaranteed generation an alarm. # Maximum occupancy value to reach for guaranteed generation an operation. Create a new measurement value.  		Generate a value that increases or decreases the total occupancy count of the building by `self.AP_VAL_CHANGE`. 		Reverse the direction of the change for the next value, if reached to the top or bottom. # Change in the occupancy count of the building # decrease the value # If the total is going to be less than zero, update the 'totalChange' so that it becomes zero. # Update the total occupancy in the building # Split the change in the occupancy into entered and exited count so that the total # change in the occupancy is still equal to the `totalChange`. `entered - exited == totalChange` # use random value for splitting # (number of people entered, number of people exited) # create measurement to send # Check if reached top or bottom # Increase the cycle count # The occupancy count should go up now. # Start decreasing the occupancy count # Start decreasing the occupancy count only if we are not in the # cycle that creates an operation