visionforge/plotly/examples/notebooks/Issue-sim.ipynb

8.2 KiB

In [ ]:
%use plotly

API

In [ ]:
enum class Severity(val penalty: Double){
    MINOR(1.0),
    MAJOR(2.0),
    CRITICAL(3.0)
}

enum class State{
    OPEN,
    ASSIGNED,
    RESOLVED
}

data class Issue(val id: String, val dayCreated: Int, val severity: Severity, val complexity: Int, 
                 var state: State = State.OPEN, var dayAssigned: Int? = null, var dayResolved: Int? = null){
    fun activate(day: Int){ 
        state = State.ASSIGNED
        dayAssigned = day
    }
    
    fun resolve(day: Int){
        state = State.RESOLVED
        dayResolved = day
    }
    
    internal fun tryResolve(day: Int){
        if(state == State.ASSIGNED && day >= (dayAssigned ?: 0) + complexity ){
            resolve(day)
        }
    }
}

class Worker(val name: String){
    var currentIssue: Issue? = null
       private set
    
    fun isBusy(): Boolean = currentIssue != null
    
    fun update(day: Int){
        currentIssue?.tryResolve(day)
        if(currentIssue?.state == State.RESOLVED){
            currentIssue = null
        }
    }
    
    fun assign(day: Int, issue: Issue){
        if(currentIssue != null) error("Can't assign work to a worker which is busy")
        issue.activate(day)
        currentIssue = issue
    }
}

interface IssueGenerator{
    fun generate(day: Int): List<Issue>
}

interface Strategy{
    fun selectIssue(day: Int, issues: List<Issue>): Issue?
}

class WorkResult(val issues: List<Issue>, val workers: Int, val days: Int)

@OptIn(kotlin.ExperimentalStdlibApi::class)
fun simulate(generator: IssueGenerator, strategy: Strategy, numWorkers: Int = 10, days: Int = 100): WorkResult{
    val workers = (0 until numWorkers).map{Worker("worker $it")}
    val issues =  buildList<Issue>{
        for(day in 0 until days){
            //update all workers
            workers.forEach { it.update(day) }
            //generate new issues
            val newIssues = generator.generate(day)
            addAll(newIssues)
            //Select all free workers
            workers.filter { !it.isBusy() }.forEach { worker->
                val unasigned = filter { it.state == State.OPEN }
                val anIssue = strategy.selectIssue(day, unasigned) //select an issue to assign from all unassigned issues
                if(anIssue != null){
                    worker.assign(day, anIssue)
                }
            }
        }
    }
    return WorkResult(issues, numWorkers, days)
}

fun WorkResult.computeLoss(): Double = issues.sumByDouble { ((it.dayResolved ?: days) - it.dayCreated)*it.severity.penalty } / days / workers / issues.size

Implementations

In [ ]:
import kotlin.random.Random
import kotlin.math.pow

/**
* Generate one random issue per day
*/
class RandomIssueGenerator(seed: Long, val issuesPerDay: Int = 4 ) : IssueGenerator{
    private val random = Random(seed)
    override fun generate(day: Int): List<Issue>{
        return List(issuesPerDay){
            val severity = Severity.values()[random.nextInt(3)]
            val complexity = random.nextInt(15)
            Issue("${day}_${it}", day, severity, complexity)
        }
    }
}

object TakeOldest: Strategy{
    override fun selectIssue(day: Int, issues: List<Issue>): Issue?{
        return issues.minByOrNull { it.dayCreated }
    }
}

class TakeRandom(seed: Long): Strategy{
    private val random = Random(seed)
    override fun selectIssue(day: Int, issues: List<Issue>): Issue?{
        if(issues.isEmpty()) return null
        return issues.random(random)
    }
}

object TakeCritical: Strategy{
    override fun selectIssue(day: Int, issues: List<Issue>): Issue?{
        return  issues.maxByOrNull { it.severity.penalty*(day - it.dayCreated) }
    }
}

Simulate lossseverity

In [ ]:
val seed = 89L
val days = 100
val workers = 10

Take oldest

In [ ]:
val result = simulate(RandomIssueGenerator(seed, workers),TakeOldest, days = days)
//result.issues.forEach { println(it)}
result.computeLoss()

Take random

In [ ]:
simulate(RandomIssueGenerator(seed, workers),TakeRandom(seed), days = days).computeLoss()

Take critical

In [ ]:
simulate(RandomIssueGenerator(seed, workers), TakeCritical, days = days).computeLoss()
In [ ]:
val seeds = List(1000){Random.nextLong()}

Plotly.plot{
    trace{
        x.numbers = seeds.map{ seed -> simulate(RandomIssueGenerator(seed, workers), TakeOldest, days = days).computeLoss()}
        name = "oldest"
        type = TraceType.histogram
    }
    trace{
        x.numbers = seeds.map{ seed -> simulate(RandomIssueGenerator(seed, workers), TakeRandom(seed), days = days).computeLoss()}
        name = "random"
        type = TraceType.histogram
    }
    trace{
        x.numbers = seeds.map{ seed -> simulate(RandomIssueGenerator(seed, workers), TakeCritical, days = days).computeLoss()}
        name = "critical"
        type = TraceType.histogram
    }
    layout{
        title = "Loss distribtution"
        xaxis {
            title = "Loss"
        }
    }
}
In [ ]:

In [ ]: