diff options
| author | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2021-06-14 13:57:25 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-06-14 13:57:25 +0200 |
| commit | e24e7c1601257cf27d71db8ca3ae273b24ab06ed (patch) | |
| tree | 913777f9e7c0008abaaf18445932583e4b01c972 | |
| parent | e11cc719201b1e09a30fc88a30524219a17a1af0 (diff) | |
| parent | b5826e9dcf4a6b510d26168ba02b1781b3b6c521 (diff) | |
simulator: Add energy modelling subsystem to OpenDC
This pull request adds a subsystem to OpenDC for modelling power components in datacenters,
such as UPSes, PDUs and PSUs.
These components also take into account electrical losses that occur in real-world scenarios.
- Add module for datacenter power components (UPS, PDU)
- Integrate power subsystem with compute subsystem (PSU)
- Model power loss in power components
**Breaking API Changes**
1. `SimBareMetalMachine.powerDraw` is replaced by `SimBareMetalMachine.psu.powerDraw`
19 files changed, 1035 insertions, 23 deletions
diff --git a/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimHost.kt b/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimHost.kt index d36717af..540e27fe 100644 --- a/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimHost.kt +++ b/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimHost.kt @@ -129,7 +129,7 @@ public class SimHost( _batch.put(_cpuWorkInterference, interferedWork.toDouble()) _batch.put(_cpuUsage, cpuUsage) _batch.put(_cpuDemand, cpuDemand) - _batch.put(_cpuPower, machine.powerDraw) + _batch.put(_cpuPower, machine.psu.powerDraw) _batch.record() } } diff --git a/opendc-experiments/opendc-experiments-tf20/src/main/kotlin/org/opendc/experiments/tf20/core/SimTFDevice.kt b/opendc-experiments/opendc-experiments-tf20/src/main/kotlin/org/opendc/experiments/tf20/core/SimTFDevice.kt index 001547ef..92e7080a 100644 --- a/opendc-experiments/opendc-experiments-tf20/src/main/kotlin/org/opendc/experiments/tf20/core/SimTFDevice.kt +++ b/opendc-experiments/opendc-experiments-tf20/src/main/kotlin/org/opendc/experiments/tf20/core/SimTFDevice.kt @@ -153,7 +153,7 @@ public class SimTFDevice( } SimResourceEvent.Run -> { _usage.record(ctx.speed) - _power.record(machine.powerDraw) + _power.record(machine.psu.powerDraw) } else -> {} } diff --git a/opendc-simulator/opendc-simulator-compute/build.gradle.kts b/opendc-simulator/opendc-simulator-compute/build.gradle.kts index 4eb0be33..41cdd40c 100644 --- a/opendc-simulator/opendc-simulator-compute/build.gradle.kts +++ b/opendc-simulator/opendc-simulator-compute/build.gradle.kts @@ -32,6 +32,7 @@ plugins { dependencies { api(platform(projects.opendcPlatform)) api(projects.opendcSimulator.opendcSimulatorResources) + api(projects.opendcSimulator.opendcSimulatorPower) implementation(projects.opendcSimulator.opendcSimulatorCore) implementation(projects.opendcUtils) implementation(libs.yaml) diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimBareMetalMachine.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimBareMetalMachine.kt index 642873fd..7f416010 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimBareMetalMachine.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimBareMetalMachine.kt @@ -31,38 +31,35 @@ import org.opendc.simulator.resources.SimResourceInterpreter * A simulated bare-metal machine that is able to run a single workload. * * A [SimBareMetalMachine] is a stateful object and you should be careful when operating this object concurrently. For - * example. The class expects only a single concurrent call to [run]. + * example, the class expects only a single concurrent call to [run]. * * @param interpreter The [SimResourceInterpreter] to drive the simulation. * @param model The machine model to simulate. * @param powerDriver The power driver to use. + * @param psu The power supply of the machine. * @param parent The parent simulation system. */ public class SimBareMetalMachine( interpreter: SimResourceInterpreter, model: SimMachineModel, powerDriver: PowerDriver, + public val psu: SimPsu = SimPsu(500.0, mapOf(1.0 to 1.0)), parent: SimResourceSystem? = null, ) : SimAbstractMachine(interpreter, parent, model) { + /** + * The processing units of the machine. + */ override val cpus: List<SimProcessingUnit> = model.cpus.map { cpu -> Cpu(SimResourceSource(cpu.frequency, interpreter, this@SimBareMetalMachine), cpu) } - /** - * Construct the [PowerDriver.Logic] for this machine. - */ - private val powerDriver = powerDriver.createLogic(this, cpus) - - /** - * The power draw of the machine. - */ - public var powerDraw: Double = 0.0 - private set - override fun updateUsage(usage: Double) { super.updateUsage(usage) + psu.update() + } - powerDraw = powerDriver.computePower() + init { + psu.connect(powerDriver.createLogic(this, cpus)) } /** diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimPsu.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimPsu.kt new file mode 100644 index 00000000..4ddad1c9 --- /dev/null +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimPsu.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.simulator.compute + +import org.opendc.simulator.compute.power.PowerDriver +import org.opendc.simulator.power.SimPowerInlet +import org.opendc.simulator.resources.SimResourceCommand +import org.opendc.simulator.resources.SimResourceConsumer +import org.opendc.simulator.resources.SimResourceContext +import org.opendc.simulator.resources.SimResourceEvent +import java.util.* + +/** + * A power supply of a [SimBareMetalMachine]. + * + * @param ratedOutputPower The rated output power of the PSU. + * @param energyEfficiency The energy efficiency of the PSU for various power draws. + */ +public class SimPsu( + private val ratedOutputPower: Double, + energyEfficiency: Map<Double, Double>, +) : SimPowerInlet() { + /** + * The power draw of the machine at this instant. + */ + public val powerDraw: Double + get() = _powerDraw + private var _powerDraw = 0.0 + + /** + * The energy efficiency of the PSU at various power draws. + */ + private val energyEfficiency = TreeMap(energyEfficiency) + + /** + * The consumer context. + */ + private var _ctx: SimResourceContext? = null + + /** + * The driver that is connected to the PSU. + */ + private var _driver: PowerDriver.Logic? = null + + init { + require(energyEfficiency.isNotEmpty()) { "Must specify at least one entry for energy efficiency of PSU" } + } + + /** + * Update the power draw of the PSU. + */ + public fun update() { + _ctx?.interrupt() + } + + /** + * Connect the specified [PowerDriver.Logic] to this PSU. + */ + public fun connect(driver: PowerDriver.Logic) { + check(_driver == null) { "PSU already connected" } + _driver = driver + update() + } + + override fun createConsumer(): SimResourceConsumer = object : SimResourceConsumer { + override fun onNext(ctx: SimResourceContext): SimResourceCommand { + val powerDraw = computePowerDraw(_driver?.computePower() ?: 0.0) + + return if (powerDraw > 0.0) + SimResourceCommand.Consume(Double.POSITIVE_INFINITY, powerDraw, Long.MAX_VALUE) + else + SimResourceCommand.Idle() + } + + override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { + when (event) { + SimResourceEvent.Start -> _ctx = ctx + SimResourceEvent.Run -> _powerDraw = ctx.speed + SimResourceEvent.Exit -> _ctx = null + else -> {} + } + } + } + + /** + * Compute the power draw of the PSU including the power loss. + */ + private fun computePowerDraw(load: Double): Double { + val loadPercentage = (load / ratedOutputPower).coerceIn(0.0, 1.0) + val efficiency = energyEfficiency.ceilingEntry(loadPercentage)?.value ?: 1.0 + return load / efficiency + } + + override fun toString(): String = "SimPsu[draw=$_powerDraw]" +} diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/power/PowerDriver.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/power/PowerDriver.kt index a1a2b911..1a46dd4a 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/power/PowerDriver.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/power/PowerDriver.kt @@ -26,22 +26,22 @@ import org.opendc.simulator.compute.SimMachine import org.opendc.simulator.compute.SimProcessingUnit /** - * A [PowerDriver] is responsible for switching the processor to the correct frequency. + * A [PowerDriver] is responsible for tracking the power usage for a component of the machine. */ public interface PowerDriver { /** - * Create the scaling logic for the specified [machine] + * Create the driver logic for the specified [machine]. */ public fun createLogic(machine: SimMachine, cpus: List<SimProcessingUnit>): Logic /** - * The logic of the scaling driver. + * The logic of the power driver. */ public interface Logic { /** - * Compute the power consumption of the processor. + * Compute the power consumption of the component. * - * @return The power consumption of the processor in W. + * @return The power consumption of the component in W. */ public fun computePower(): Double } diff --git a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimMachineTest.kt b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimMachineTest.kt index 0c686aa0..b9cfb06b 100644 --- a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimMachineTest.kt +++ b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimMachineTest.kt @@ -40,6 +40,7 @@ import org.opendc.simulator.compute.util.SimWorkloadLifecycle import org.opendc.simulator.compute.workload.SimFlopsWorkload import org.opendc.simulator.compute.workload.SimWorkload import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.power.SimPowerSource import org.opendc.simulator.resources.SimResourceInterpreter import org.opendc.simulator.resources.consumer.SimWorkConsumer @@ -142,16 +143,20 @@ class SimMachineTest { @Test fun testPower() = runBlockingSimulation { + val interpreter = SimResourceInterpreter(coroutineContext, clock) val machine = SimBareMetalMachine( - SimResourceInterpreter(coroutineContext, clock), + interpreter, machineModel, SimplePowerDriver(LinearPowerModel(100.0, 50.0)) ) + val source = SimPowerSource(interpreter, capacity = 1000.0) + source.connect(machine.psu) try { coroutineScope { launch { machine.run(SimFlopsWorkload(2_000, utilization = 1.0)) } - assertEquals(100.0, machine.powerDraw) + assertEquals(100.0, machine.psu.powerDraw) + assertEquals(100.0, source.powerDraw) } } finally { machine.close() diff --git a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimPsuTest.kt b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimPsuTest.kt new file mode 100644 index 00000000..e0ebdb73 --- /dev/null +++ b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimPsuTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.simulator.compute + +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.opendc.simulator.compute.power.PowerDriver +import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.power.SimPowerSource +import org.opendc.simulator.resources.SimResourceInterpreter + +/** + * Test suite for [SimPsu] + */ +internal class SimPsuTest { + + @Test + fun testInvalidInput() { + assertThrows<IllegalArgumentException> { SimPsu(1.0, emptyMap()) } + } + + @Test + fun testDoubleConnect() { + val psu = SimPsu(1.0, mapOf(0.0 to 1.0)) + val cpuLogic = mockk<PowerDriver.Logic>() + psu.connect(cpuLogic) + assertThrows<IllegalStateException> { psu.connect(mockk()) } + } + + @Test + fun testPsuIdle() = runBlockingSimulation { + val ratedOutputPower = 240.0 + val energyEfficiency = mapOf(0.0 to 1.0) + + val interpreter = SimResourceInterpreter(coroutineContext, clock) + val source = SimPowerSource(interpreter, capacity = ratedOutputPower) + + val cpuLogic = mockk<PowerDriver.Logic>() + every { cpuLogic.computePower() } returns 0.0 + + val psu = SimPsu(ratedOutputPower, energyEfficiency) + psu.connect(cpuLogic) + source.connect(psu) + + assertEquals(0.0, source.powerDraw, 0.01) + } + + @Test + fun testPsuPowerLoss() = runBlockingSimulation { + val ratedOutputPower = 240.0 + // Efficiency of 80 Plus Titanium PSU + val energyEfficiency = sortedMapOf( + 0.3 to 0.9, + 0.7 to 0.92, + 1.0 to 0.94, + ) + + val interpreter = SimResourceInterpreter(coroutineContext, clock) + val source = SimPowerSource(interpreter, capacity = ratedOutputPower) + + val cpuLogic = mockk<PowerDriver.Logic>() + every { cpuLogic.computePower() } returnsMany listOf(50.0, 100.0, 150.0, 200.0) + + val psu = SimPsu(ratedOutputPower, energyEfficiency) + psu.connect(cpuLogic) + source.connect(psu) + + assertEquals(55.55, source.powerDraw, 0.01) + + psu.update() + assertEquals(108.695, source.powerDraw, 0.01) + + psu.update() + assertEquals(163.043, source.powerDraw, 0.01) + + psu.update() + assertEquals(212.765, source.powerDraw, 0.01) + } +} diff --git a/opendc-simulator/opendc-simulator-power/build.gradle.kts b/opendc-simulator/opendc-simulator-power/build.gradle.kts new file mode 100644 index 00000000..f2a49964 --- /dev/null +++ b/opendc-simulator/opendc-simulator-power/build.gradle.kts @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +description = "Library for simulating datacenter power components" + +plugins { + `kotlin-library-conventions` + `testing-conventions` + `jacoco-conventions` +} + +dependencies { + api(platform(projects.opendcPlatform)) + api(projects.opendcSimulator.opendcSimulatorResources) + implementation(projects.opendcSimulator.opendcSimulatorCore) +} diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt new file mode 100644 index 00000000..11034a57 --- /dev/null +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.simulator.power + +import org.opendc.simulator.resources.* + +/** + * A model of a Power Distribution Unit (PDU). + * + * @param interpreter The underlying [SimResourceInterpreter] to drive the simulation under the hood. + * @param idlePower The idle power consumption of the PDU independent of the load on the PDU. + * @param lossCoefficient The coefficient for the power loss of the PDU proportional to the square load. + */ +public class SimPdu( + interpreter: SimResourceInterpreter, + private val idlePower: Double = 0.0, + private val lossCoefficient: Double = 0.0, +) : SimPowerInlet() { + /** + * The [SimResourceDistributor] that distributes the electricity over the PDU outlets. + */ + private val distributor = SimResourceDistributorMaxMin(interpreter) + + /** + * Create a new PDU outlet. + */ + public fun newOutlet(): Outlet = Outlet(distributor.newOutput()) + + override fun createConsumer(): SimResourceConsumer = object : SimResourceConsumer by distributor { + override fun onNext(ctx: SimResourceContext): SimResourceCommand { + return when (val cmd = distributor.onNext(ctx)) { + is SimResourceCommand.Consume -> { + val duration = cmd.work / cmd.limit + val loss = computePowerLoss(cmd.limit) + val newLimit = cmd.limit + loss + + SimResourceCommand.Consume(duration * newLimit, newLimit, cmd.deadline) + } + is SimResourceCommand.Idle -> { + val loss = computePowerLoss(0.0) + if (loss > 0.0) + SimResourceCommand.Consume(Double.POSITIVE_INFINITY, loss, cmd.deadline) + else + cmd + } + else -> cmd + } + } + + override fun toString(): String = "SimPdu.Consumer" + } + + override fun toString(): String = "SimPdu" + + /** + * Compute the power loss that occurs in the PDU. + */ + private fun computePowerLoss(load: Double): Double { + // See https://download.schneider-electric.com/files?p_Doc_Ref=SPD_NRAN-66CK3D_EN + return idlePower + lossCoefficient * (load * load) + } + + /** + * A PDU outlet. + */ + public class Outlet(private val provider: SimResourceProvider) : SimPowerOutlet(), AutoCloseable { + override fun onConnect(inlet: SimPowerInlet) { + provider.startConsumer(inlet.createConsumer()) + } + + override fun onDisconnect(inlet: SimPowerInlet) { + provider.cancel() + } + + /** + * Remove the outlet from the PDU. + */ + override fun close() { + provider.close() + } + + override fun toString(): String = "SimPdu.Outlet" + } +} diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerInlet.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerInlet.kt new file mode 100644 index 00000000..0ac1f199 --- /dev/null +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerInlet.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.simulator.power + +import org.opendc.simulator.resources.SimResourceConsumer + +/** + * An abstract inlet that consumes electricity from a power outlet. + */ +public abstract class SimPowerInlet { + /** + * A flag to indicate that the inlet is currently connected to an outlet. + */ + public val isConnected: Boolean + get() = _outlet != null + + /** + * The [SimPowerOutlet] to which the inlet is connected. + */ + public val outlet: SimPowerOutlet? + get() = _outlet + internal var _outlet: SimPowerOutlet? = null + + /** + * Create a [SimResourceConsumer] which represents the consumption of electricity from the power outlet. + */ + public abstract fun createConsumer(): SimResourceConsumer +} diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerOutlet.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerOutlet.kt new file mode 100644 index 00000000..72f52acc --- /dev/null +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerOutlet.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.simulator.power + +/** + * An abstract outlet that provides a source of electricity for datacenter components. + */ +public abstract class SimPowerOutlet { + /** + * A flag to indicate that the inlet is currently connected to an outlet. + */ + public val isConnected: Boolean + get() = _inlet != null + + /** + * The inlet that is connected to this outlet currently. + */ + public val inlet: SimPowerInlet? + get() = _inlet + private var _inlet: SimPowerInlet? = null + + /** + * Connect the specified power [inlet] to this outlet. + * + * @param inlet The inlet to connect to the outlet. + */ + public fun connect(inlet: SimPowerInlet) { + check(!isConnected) { "Outlet already connected" } + check(!inlet.isConnected) { "Inlet already connected" } + + _inlet = inlet + inlet._outlet = this + + onConnect(inlet) + } + + /** + * Disconnect the connected power outlet from this inlet + */ + public fun disconnect() { + val inlet = _inlet + if (inlet != null) { + _inlet = null + assert(inlet._outlet == this) { "Inlet state incorrect" } + inlet._outlet = null + + onDisconnect(inlet) + } + } + + /** + * This method is invoked when an inlet is connected to the outlet. + */ + protected abstract fun onConnect(inlet: SimPowerInlet) + + /** + * This method is invoked when an inlet is disconnected from the outlet. + */ + protected abstract fun onDisconnect(inlet: SimPowerInlet) +} diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerSource.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerSource.kt new file mode 100644 index 00000000..3ef8ccc6 --- /dev/null +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerSource.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.simulator.power + +import org.opendc.simulator.resources.SimResourceInterpreter +import org.opendc.simulator.resources.SimResourceSource + +/** + * A [SimPowerOutlet] that represents a source of electricity. + * + * @param interpreter The underlying [SimResourceInterpreter] to drive the simulation under the hood. + */ +public class SimPowerSource(interpreter: SimResourceInterpreter, public val capacity: Double) : SimPowerOutlet() { + /** + * The resource source that drives this power source. + */ + private val source = SimResourceSource(capacity, interpreter) + + /** + * The power draw at this instant. + */ + public val powerDraw: Double + get() = source.speed + + override fun onConnect(inlet: SimPowerInlet) { + source.startConsumer(inlet.createConsumer()) + } + + override fun onDisconnect(inlet: SimPowerInlet) { + source.cancel() + } + + override fun toString(): String = "SimPowerSource" +} diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt new file mode 100644 index 00000000..f9431d21 --- /dev/null +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.simulator.power + +import org.opendc.simulator.resources.* + +/** + * A model of an Uninterruptible Power Supply (UPS). + * + * This model aggregates multiple power sources into a single source in order to ensure that power is always available. + * + * @param interpreter The underlying [SimResourceInterpreter] to drive the simulation under the hood. + * @param idlePower The idle power consumption of the UPS independent of the load. + * @param lossCoefficient The coefficient for the power loss of the UPS proportional to the load. + */ +public class SimUps( + interpreter: SimResourceInterpreter, + private val idlePower: Double = 0.0, + private val lossCoefficient: Double = 0.0, +) : SimPowerOutlet() { + /** + * The resource aggregator used to combine the input sources. + */ + private val aggregator = SimResourceAggregatorMaxMin(interpreter) + + /** + * Create a new UPS outlet. + */ + public fun newInlet(): SimPowerInlet { + val forward = SimResourceForwarder(isCoupled = true) + aggregator.addInput(forward) + return Inlet(forward) + } + + override fun onConnect(inlet: SimPowerInlet) { + val consumer = inlet.createConsumer() + aggregator.startConsumer(object : SimResourceConsumer by consumer { + override fun onNext(ctx: SimResourceContext): SimResourceCommand { + return when (val cmd = consumer.onNext(ctx)) { + is SimResourceCommand.Consume -> { + val duration = cmd.work / cmd.limit + val loss = computePowerLoss(cmd.limit) + val newLimit = cmd.limit + loss + + SimResourceCommand.Consume(duration * newLimit, newLimit, cmd.deadline) + } + is SimResourceCommand.Idle -> { + val loss = computePowerLoss(0.0) + if (loss > 0.0) + SimResourceCommand.Consume(Double.POSITIVE_INFINITY, loss, cmd.deadline) + else + cmd + } + else -> cmd + } + } + }) + } + + override fun onDisconnect(inlet: SimPowerInlet) { + aggregator.cancel() + } + + /** + * Compute the power loss that occurs in the UPS. + */ + private fun computePowerLoss(load: Double): Double { + // See https://download.schneider-electric.com/files?p_Doc_Ref=SPD_NRAN-66CK3D_EN + return idlePower + lossCoefficient * load + } + + /** + * A UPS inlet. + */ + public inner class Inlet(private val forwarder: SimResourceTransformer) : SimPowerInlet(), AutoCloseable { + override fun createConsumer(): SimResourceConsumer = forwarder + + /** + * Remove the inlet from the PSU. + */ + override fun close() { + forwarder.close() + } + + override fun toString(): String = "SimPsu.Inlet" + } +} diff --git a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt new file mode 100644 index 00000000..17a174b7 --- /dev/null +++ b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.simulator.power + +import io.mockk.spyk +import io.mockk.verify +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.resources.SimResourceConsumer +import org.opendc.simulator.resources.SimResourceEvent +import org.opendc.simulator.resources.SimResourceInterpreter +import org.opendc.simulator.resources.consumer.SimWorkConsumer + +/** + * Test suite for the [SimPdu] class. + */ +internal class SimPduTest { + @Test + fun testZeroOutlets() = runBlockingSimulation { + val interpreter = SimResourceInterpreter(coroutineContext, clock) + val source = SimPowerSource(interpreter, capacity = 100.0) + val pdu = SimPdu(interpreter) + source.connect(pdu) + + assertEquals(0.0, source.powerDraw) + } + + @Test + fun testSingleOutlet() = runBlockingSimulation { + val interpreter = SimResourceInterpreter(coroutineContext, clock) + val source = SimPowerSource(interpreter, capacity = 100.0) + val pdu = SimPdu(interpreter) + source.connect(pdu) + pdu.newOutlet().connect(SimpleInlet()) + + assertEquals(50.0, source.powerDraw) + } + + @Test + fun testDoubleOutlet() = runBlockingSimulation { + val interpreter = SimResourceInterpreter(coroutineContext, clock) + val source = SimPowerSource(interpreter, capacity = 100.0) + val pdu = SimPdu(interpreter) + source.connect(pdu) + + pdu.newOutlet().connect(SimpleInlet()) + pdu.newOutlet().connect(SimpleInlet()) + + assertEquals(100.0, source.powerDraw) + } + + @Test + fun testDisconnect() = runBlockingSimulation { + val interpreter = SimResourceInterpreter(coroutineContext, clock) + val source = SimPowerSource(interpreter, capacity = 100.0) + val pdu = SimPdu(interpreter) + source.connect(pdu) + val consumer = spyk(SimWorkConsumer(100.0, utilization = 1.0)) + val inlet = object : SimPowerInlet() { + override fun createConsumer(): SimResourceConsumer = consumer + } + + val outlet = pdu.newOutlet() + outlet.connect(inlet) + outlet.disconnect() + + verify { consumer.onEvent(any(), SimResourceEvent.Exit) } + } + + @Test + fun testLoss() = runBlockingSimulation { + val interpreter = SimResourceInterpreter(coroutineContext, clock) + val source = SimPowerSource(interpreter, capacity = 100.0) + // https://download.schneider-electric.com/files?p_Doc_Ref=SPD_NRAN-66CK3D_EN + val pdu = SimPdu(interpreter, idlePower = 1.5, lossCoefficient = 0.015) + source.connect(pdu) + pdu.newOutlet().connect(SimpleInlet()) + assertEquals(89.0, source.powerDraw, 0.01) + } + + @Test + fun testOutletClose() = runBlockingSimulation { + val interpreter = SimResourceInterpreter(coroutineContext, clock) + val source = SimPowerSource(interpreter, capacity = 100.0) + val pdu = SimPdu(interpreter) + source.connect(pdu) + val outlet = pdu.newOutlet() + outlet.close() + + assertThrows<IllegalStateException> { + outlet.connect(SimpleInlet()) + } + } + + class SimpleInlet : SimPowerInlet() { + override fun createConsumer(): SimResourceConsumer = SimWorkConsumer(100.0, utilization = 0.5) + } +} diff --git a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPowerSourceTest.kt b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPowerSourceTest.kt new file mode 100644 index 00000000..f3829ba1 --- /dev/null +++ b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPowerSourceTest.kt @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.simulator.power + +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.resources.SimResourceConsumer +import org.opendc.simulator.resources.SimResourceEvent +import org.opendc.simulator.resources.SimResourceInterpreter +import org.opendc.simulator.resources.consumer.SimWorkConsumer + +/** + * Test suite for the [SimPowerSource] + */ +internal class SimPowerSourceTest { + @Test + fun testInitialState() = runBlockingSimulation { + val interpreter = SimResourceInterpreter(coroutineContext, clock) + val source = SimPowerSource(interpreter, capacity = 100.0) + + assertFalse(source.isConnected) + assertNull(source.inlet) + assertEquals(100.0, source.capacity) + } + + @Test + fun testDisconnectIdempotent() = runBlockingSimulation { + val interpreter = SimResourceInterpreter(coroutineContext, clock) + val source = SimPowerSource(interpreter, capacity = 100.0) + + assertDoesNotThrow { source.disconnect() } + assertFalse(source.isConnected) + } + + @Test + fun testConnect() = runBlockingSimulation { + val interpreter = SimResourceInterpreter(coroutineContext, clock) + val source = SimPowerSource(interpreter, capacity = 100.0) + val inlet = SimpleInlet() + + source.connect(inlet) + + assertTrue(source.isConnected) + assertEquals(inlet, source.inlet) + assertTrue(inlet.isConnected) + assertEquals(source, inlet.outlet) + assertEquals(100.0, source.powerDraw) + } + + @Test + fun testDisconnect() = runBlockingSimulation { + val interpreter = SimResourceInterpreter(coroutineContext, clock) + val source = SimPowerSource(interpreter, capacity = 100.0) + val consumer = spyk(SimWorkConsumer(100.0, utilization = 1.0)) + val inlet = object : SimPowerInlet() { + override fun createConsumer(): SimResourceConsumer = consumer + } + + source.connect(inlet) + source.disconnect() + + verify { consumer.onEvent(any(), SimResourceEvent.Exit) } + } + + @Test + fun testDisconnectAssertion() = runBlockingSimulation { + val interpreter = SimResourceInterpreter(coroutineContext, clock) + val source = SimPowerSource(interpreter, capacity = 100.0) + val inlet = mockk<SimPowerInlet>(relaxUnitFun = true) + every { inlet.isConnected } returns false + every { inlet._outlet } returns null + every { inlet.createConsumer() } returns SimWorkConsumer(100.0, utilization = 1.0) + + source.connect(inlet) + + assertThrows<AssertionError> { + source.disconnect() + } + } + + @Test + fun testOutletAlreadyConnected() = runBlockingSimulation { + val interpreter = SimResourceInterpreter(coroutineContext, clock) + val source = SimPowerSource(interpreter, capacity = 100.0) + val inlet = SimpleInlet() + + source.connect(inlet) + assertThrows<IllegalStateException> { + source.connect(SimpleInlet()) + } + + assertEquals(inlet, source.inlet) + } + + @Test + fun testInletAlreadyConnected() = runBlockingSimulation { + val interpreter = SimResourceInterpreter(coroutineContext, clock) + val source = SimPowerSource(interpreter, capacity = 100.0) + val inlet = mockk<SimPowerInlet>(relaxUnitFun = true) + every { inlet.isConnected } returns true + + assertThrows<IllegalStateException> { + source.connect(inlet) + } + } + + class SimpleInlet : SimPowerInlet() { + override fun createConsumer(): SimResourceConsumer = SimWorkConsumer(100.0, utilization = 1.0) + } +} diff --git a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimUpsTest.kt b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimUpsTest.kt new file mode 100644 index 00000000..8d5fa857 --- /dev/null +++ b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimUpsTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.simulator.power + +import io.mockk.spyk +import io.mockk.verify +import org.junit.jupiter.api.Assertions.assertAll +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.resources.SimResourceConsumer +import org.opendc.simulator.resources.SimResourceEvent +import org.opendc.simulator.resources.SimResourceInterpreter +import org.opendc.simulator.resources.consumer.SimWorkConsumer + +/** + * Test suite for the [SimUps] class. + */ +internal class SimUpsTest { + @Test + fun testSingleInlet() = runBlockingSimulation { + val interpreter = SimResourceInterpreter(coroutineContext, clock) + val source = SimPowerSource(interpreter, capacity = 100.0) + val ups = SimUps(interpreter) + source.connect(ups.newInlet()) + ups.connect(SimpleInlet()) + + assertEquals(50.0, source.powerDraw) + } + + @Test + fun testDoubleInlet() = runBlockingSimulation { + val interpreter = SimResourceInterpreter(coroutineContext, clock) + val source1 = SimPowerSource(interpreter, capacity = 100.0) + val source2 = SimPowerSource(interpreter, capacity = 100.0) + val ups = SimUps(interpreter) + source1.connect(ups.newInlet()) + source2.connect(ups.newInlet()) + + ups.connect(SimpleInlet()) + + assertAll( + { assertEquals(50.0, source1.powerDraw) }, + { assertEquals(50.0, source2.powerDraw) } + ) + } + + @Test + fun testLoss() = runBlockingSimulation { + val interpreter = SimResourceInterpreter(coroutineContext, clock) + val source = SimPowerSource(interpreter, capacity = 100.0) + // https://download.schneider-electric.com/files?p_Doc_Ref=SPD_NRAN-66CK3D_EN + val ups = SimUps(interpreter, idlePower = 4.0, lossCoefficient = 0.05) + source.connect(ups.newInlet()) + ups.connect(SimpleInlet()) + + assertEquals(56.5, source.powerDraw) + } + + @Test + fun testDisconnect() = runBlockingSimulation { + val interpreter = SimResourceInterpreter(coroutineContext, clock) + val source1 = SimPowerSource(interpreter, capacity = 100.0) + val source2 = SimPowerSource(interpreter, capacity = 100.0) + val ups = SimUps(interpreter) + source1.connect(ups.newInlet()) + source2.connect(ups.newInlet()) + val consumer = spyk(SimWorkConsumer(100.0, utilization = 1.0)) + val inlet = object : SimPowerInlet() { + override fun createConsumer(): SimResourceConsumer = consumer + } + + ups.connect(inlet) + ups.disconnect() + + verify { consumer.onEvent(any(), SimResourceEvent.Exit) } + } + + class SimpleInlet : SimPowerInlet() { + override fun createConsumer(): SimResourceConsumer = SimWorkConsumer(100.0, utilization = 0.5) + } +} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSource.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSource.kt index 9f062cc3..2f70e3cc 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSource.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSource.kt @@ -44,7 +44,11 @@ public class SimResourceSource( } override fun onConsume(ctx: SimResourceControllableContext, work: Double, limit: Double, deadline: Long): Long { - return min(deadline, ctx.clock.millis() + getDuration(work, speed)) + return if (work.isInfinite()) { + Long.MAX_VALUE + } else { + min(deadline, ctx.clock.millis() + getDuration(work, speed)) + } } override fun onUpdate(ctx: SimResourceControllableContext, work: Double) { diff --git a/settings.gradle.kts b/settings.gradle.kts index d09e2912..3b0571b8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -40,6 +40,7 @@ include(":opendc-web:opendc-web-ui") include(":opendc-web:opendc-web-runner") include(":opendc-simulator:opendc-simulator-core") include(":opendc-simulator:opendc-simulator-resources") +include(":opendc-simulator:opendc-simulator-power") include(":opendc-simulator:opendc-simulator-compute") include(":opendc-simulator:opendc-simulator-failures") include(":opendc-telemetry:opendc-telemetry-api") |
