I wrote a complex Kotlin class Env
in which I need to implement a deepCopy method. Here is a minimal reproductive example of the class and its referenced classes:
@Serializable
class Env(
override var time: Int,
override var workstationPool: List<Workstation>,
override var jobPool: MutableList<Order>,
override val jobArrivalEvents: List<JobArrivalEvent>,
override val breakdownEvents: List<BreakdownEvent>,
) : AbstractEnvironment() {
override fun step(actionMap: Pair<MutableMap<Long, OrderAction>, MutableMap<Long, OperationDetails?>>)
: Triple<Boolean, List<Workstation>, MutableList<Order>> {
handleJobEvents()
handleDisruptiveEvents()
for (workstation in workstationPool) {
if (actionMap.second[workstation.id] != null) {
workstation.allocateOperation(actionMap.second[workstation.id]!!, time)
}
workstation.process(time)
}
val timeBufferList = mutableListOf<Int>()
for (job in jobPool) {
if (actionMap.first[job.id] == OrderAction.RELEASE && job.getState() == OrderState.RESTRICTED) {
logger.info { "Releasing order ${job.name}" }
job.release(time)
}
job.updateOrder(time)
timeBufferList.addAll(job.getTimeBufferList())
}
jobPool.forEach { job ->
job.updateShopCapacity(
workstationPool.count { it.state != WorkstationState.FAILURE },
jobPool.count { it.getState() == OrderState.PROCESSING }
)
job.details.operations.filter { it.assignee == null }.forEach { operationDetails ->
val relTimeBuffer = if (timeBufferList.max() != 0) operationDetails.timeBuffer.div(timeBufferList.max().toFloat()) else 1F
operationDetails.relTimeBuffer = relTimeBuffer
}
}
time += 1
val terminated = jobPool.all { it.getState() == OrderState.DONE }
return Triple(terminated, workstationPool, jobPool)
}
fun deepCopy(): DiscreteSchedulingEnv {
return deepCopy(this)
}
}
@Serializable
data class WorkstationDetails(
val id: Long,
val name: String,
@EncodeDefault var state: WorkstationState = WorkstationState.IDLE,
@EncodeDefault var workload: Int = 0,
@EncodeDefault var numSetupChanges: Int = 0,
@EncodeDefault var operationLog: MutableList<OperationDetails> = mutableListOf(),
@EncodeDefault var downtime: Int = 0,
@EncodeDefault val breakdownEvents: MutableList<BreakdownEvent> = mutableListOf()
) {}
@Serializable
class Workstation(val assetDetails: WorkstationDetails):
AbstractAsset<NoopClient>() {
override val id: Long = assetDetails.id
override val name: String = assetDetails.name
override val clientConfiguration: ClientConfiguration<NoopClient>
get() = TODO("Not yet implemented")
var state: WorkstationState
get() = assetDetails.state
private set(value) {
assetDetails.state = value
}
private var allocatedOperation: OperationDetails? = null
fun getAllocatedOperation(): OperationDetails?{
return allocatedOperation
}
fun allocateOperation(operation: OperationDetails, time: Int) {
if (state != WorkstationState.IDLE ) return
if (allocatedOperation == null) {
allocatedOperation = operation
allocatedOperation!!.startTime = time
allocatedOperation!!.assignee = id
val setupTime = operation.getSetupTimeForProductType(getProductTypeOfLastOperation())
allocatedOperation!!.setupTime = setupTime
allocatedOperation!!.remainingSetupTime = setupTime
if (getProductTypeOfLastOperation() != null){
if (getProductTypeOfLastOperation() != operation.orderDetailsType) assetDetails.numSetupChanges += 1
} else assetDetails.numSetupChanges += 1
allocatedOperation!!.plannedEndTime = time + allocatedOperation!!.setupTime + allocatedOperation!!.duration
}
}
fun process(time: Int) {
when (state) {
WorkstationState.IDLE -> processTaskAllocation()
WorkstationState.PROCESSING -> handleTaskExecution(allocatedOperation!!, time)
WorkstationState.FAILURE -> {
assetDetails.downtime += 1
if (allocatedOperation != null) {
logger.info { "Workstation $id can not process operation (ID / name) ${allocatedOperation!!.name} / ${allocatedOperation!!.name} due to an existing breakdown." }
}
}
}
}
}
@Serializable
data class OrderDetails(
val id: Long,
val name: String,
val type: ProductType,
@EncodeDefault val description: String = "",
@EncodeDefault var timeBuffer: Int = 0,
val operations: List<OperationDetails>,
@EncodeDefault var incomeTime: Int = 0,
@EncodeDefault val plannedDeliveryTime: Int = 0,
@EncodeDefault var actualDeliveryTime: Int = 0,
@EncodeDefault var releaseTime: Int = 0,
@EncodeDefault var shopCapacity: Int = 0,
@EncodeDefault var state: OrderState = OrderState.RESTRICTED
) {}
@Serializable
class Order(override val id: Long,
val productType: ProductType,
val deliveryTime: Int,
val incomeTime: Int = 0
) : AbstractAsset<NoopClient>() {
override var name: String = "$productType-order-$id"
private var operations = getOperations(productType) // fixme - getters not included in serialization
private var orderDetails: OrderDetails = OrderDetails(
id = id,
type = productType,
name = name,
operations = operations,
plannedDeliveryTime = deliveryTime,
incomeTime = incomeTime
)
override val clientConfiguration: ClientConfiguration<NoopClient>
get() = TODO("Not yet implemented")
fun getState(): OrderState {
return orderDetails.state
}
val details: OrderDetails get() = orderDetails
fun release(time: Int) {
if (orderDetails.state != OrderState.RESTRICTED) return
orderDetails.state = OrderState.PROCESSING
orderDetails.releaseTime = time
}
fun updateOrder(time: Int) {
when (orderDetails.state) {
OrderState.RESTRICTED -> {
updateDeliveryTimeBuffer(time)
logger.info { "Order $name is not released yet." }
}
OrderState.PROCESSING -> {
orderDetails.operations.filter { it.state == OperationState.RESTRICTED }.forEach { updateOperationState(it) }
if (orderDetails.operations.all { it.state == OperationState.DONE }) {
orderDetails.state = OrderState.DONE
orderDetails.actualDeliveryTime = time
orderDetails.timeBuffer = orderDetails.plannedDeliveryTime.minus(time)
logger.info { "Order $name is finished." }
}
else {
updateDeliveryTimeBuffer(time)
logger.info { "Order $name is in process." }
}
}
OrderState.DONE -> logger.info { "Order $name is finished." }
}
}
}
@Serializable
data class OperationDetails(
val id: Long,
val duration: Int,
val name: String,
val previous: List<OperationDetails>,
var cumulativeProcessTime: Int = 0,
var orderDetailsType: ProductType,
var orderDetailsName: String = "",
@EncodeDefault var assignee: Long? = null,
@EncodeDefault var startTime: Int? = null,
@EncodeDefault var timeBuffer: Int = 0,
@EncodeDefault var setupTime: Int = 0,
@EncodeDefault var remainingProcessTime: Int = duration,
@EncodeDefault var remainingSetupTime: Int = setupTime,
@EncodeDefault var plannedEndTime: Int = 0,
@EncodeDefault var actualEndTime: Int = 0,
@EncodeDefault var state: OperationState = OperationState.RESTRICTED,
@EncodeDefault var relTimeBuffer: Float = 0F,
@EncodeDefault val setupTimeMatrix: MutableMap<ProductType, List<Int>> = OrderSetupMatrix.setupTimeMatrix
) {}
@Serializable
class Operation(override val id: Long, override val name: String, val assetDetails: OperationDetails
) : AbstractAsset<NoopClient>(){
override val clientConfiguration: ClientConfiguration<NoopClient>
get() = TODO("Not yet implemented")
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Operation
if (id != other.id) return false
if (name != other.name) return false
if (assetDetails != other.assetDetails) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + assetDetails.hashCode()
return result
}
}
@Serializable
data class BreakdownEvent(
val workstationId: Long,
val startTime: Int,
var endTime: Int?
)
@Serializable
data class JobArrivalEvent(
val arrivalTime: Int,
val deliveryTime: Int,
val productType: ProductType,
)
I already tried to implement a deepCopy method by using kotlinx serialization to encode into a json and decode again afterwards:
object SerializationUtil {
inline fun <reified T> deepCopy(obj: T): T {
val json = Json
val jsonString = json.encodeToString(serializer(), obj)
return json.decodeFromString(serializer(), jsonString)
}
}
I wrote a test method to compare two Env objects. If I initialize an instance of Env and use the deepCopy method, all properties are similar. However, when using the Env.step() method (which modifies metrics like the state and timeBuffer of operations) on both instances of Env, the properties of some Operation instances differ. How can I properly deepCopy an Env
object? What am I missing?