Fix PID demo

This commit is contained in:
Alexander Nozik 2024-06-05 17:19:20 +03:00
parent 91f860adf6
commit c63c2db651
20 changed files with 121 additions and 56 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
# Created by .ignore support plugin (hsz.mobi) # Created by .ignore support plugin (hsz.mobi)
.idea/ .idea/
.gradle .gradle
.kotlin
*.iws *.iws
*.iml *.iml

View File

@ -24,7 +24,7 @@ public class Inertia<U : UnitsOfMeasurement, V : UnitsOfMeasurement>(
private var currentForce = force.value private var currentForce = force.value
private val movement = onTimer { prev, next -> private val movement = onTimer(DefaultTimer.REALTIME) { prev, next ->
val dtSeconds = (next - prev).toDouble(DurationUnit.SECONDS) val dtSeconds = (next - prev).toDouble(DurationUnit.SECONDS)
// compute new value based on velocity and acceleration from the previous step // compute new value based on velocity and acceleration from the previous step

View File

@ -7,22 +7,25 @@ import space.kscience.controls.constructor.units.*
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import kotlin.math.PI import kotlin.math.PI
public class ScrewDrive( /**
* https://en.wikipedia.org/wiki/Leadscrew
*/
public class Leadscrew(
context: Context, context: Context,
public val leverage: NumericalValue<Meters>, public val leverage: NumericalValue<Meters>,
) : ModelConstructor(context) { ) : ModelConstructor(context) {
public fun torqueToForce( public fun torqueToForce(
stateOfMomentum: DeviceState<NumericalValue<NewtonsMeters>>, stateOfTorque: DeviceState<NumericalValue<NewtonsMeters>>,
): DeviceState<NumericalValue<Newtons>> = DeviceState.map(stateOfMomentum) { momentum -> ): DeviceState<NumericalValue<Newtons>> = DeviceState.map(stateOfTorque) { torque ->
NumericalValue(momentum.value / leverage.value ) NumericalValue(torque.value / leverage.value )
} }
public fun degreesToMeters( public fun degreesToMeters(
stateOfAngle: DeviceState<NumericalValue<Degrees>>, stateOfAngle: DeviceState<NumericalValue<Degrees>>,
offset: NumericalValue<Meters> = NumericalValue(0), offset: NumericalValue<Meters> = NumericalValue(0),
): DeviceState<NumericalValue<Meters>> = DeviceState.map(stateOfAngle) { degrees -> ): DeviceState<NumericalValue<Meters>> = DeviceState.map(stateOfAngle) { degrees ->
offset + NumericalValue(degrees.value * 2 * PI / 360 *leverage.value ) offset + NumericalValue(degrees.value * 2 * PI / 360 * leverage.value )
} }
} }

View File

@ -3,12 +3,20 @@ package space.kscience.controls.vision
import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass import kotlinx.serialization.modules.subclass
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.PluginFactory import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.context.PluginTag
import space.kscience.dataforge.meta.Meta
import space.kscience.visionforge.Vision import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionPlugin import space.kscience.visionforge.VisionPlugin
public expect class ControlVisionPlugin: VisionPlugin{ public expect class ControlVisionPlugin: VisionPlugin{
public companion object: PluginFactory<ControlVisionPlugin> override val tag: PluginTag
override val visionSerializersModule: SerializersModule
public companion object: PluginFactory<ControlVisionPlugin>{
override val tag: PluginTag
override fun build(context: Context, meta: Meta): ControlVisionPlugin
}
} }
internal val controlsVisionSerializersModule = SerializersModule { internal val controlsVisionSerializersModule = SerializersModule {

View File

@ -158,7 +158,7 @@ public fun <T> Plot.plotDeviceState(
public fun Plot.plotNumberState( public fun Plot.plotNumberState(
context: Context, context: Context,
state: DeviceState<out Number>, state: DeviceState<Number>,
maxAge: Duration = defaultMaxAge, maxAge: Duration = defaultMaxAge,
maxPoints: Int = defaultMaxPoints, maxPoints: Int = defaultMaxPoints,
minPoints: Int = defaultMinPoints, minPoints: Int = defaultMinPoints,

View File

@ -43,9 +43,9 @@ private val sliderRenderer = ElementVisionRenderer<SliderVision> { name, vision:
public actual class ControlVisionPlugin : VisionPlugin() { public actual class ControlVisionPlugin : VisionPlugin() {
override val tag: PluginTag get() = Companion.tag actual override val tag: PluginTag get() = Companion.tag
override val visionSerializersModule: SerializersModule get() = controlsVisionSerializersModule actual override val visionSerializersModule: SerializersModule get() = controlsVisionSerializersModule
override fun content(target: String): Map<Name, Any> = when (target) { override fun content(target: String): Map<Name, Any> = when (target) {
ElementVisionRenderer.TYPE -> mapOf( ElementVisionRenderer.TYPE -> mapOf(
@ -57,9 +57,9 @@ public actual class ControlVisionPlugin : VisionPlugin() {
} }
public actual companion object : PluginFactory<ControlVisionPlugin> { public actual companion object : PluginFactory<ControlVisionPlugin> {
override val tag: PluginTag = PluginTag("controls.vision") actual override val tag: PluginTag = PluginTag("controls.vision")
override fun build(context: Context, meta: Meta): ControlVisionPlugin = ControlVisionPlugin() actual override fun build(context: Context, meta: Meta): ControlVisionPlugin = ControlVisionPlugin()
} }
} }

View File

@ -8,14 +8,14 @@ import space.kscience.dataforge.meta.Meta
import space.kscience.visionforge.VisionPlugin import space.kscience.visionforge.VisionPlugin
public actual class ControlVisionPlugin : VisionPlugin() { public actual class ControlVisionPlugin : VisionPlugin() {
override val tag: PluginTag get() = Companion.tag actual override val tag: PluginTag get() = Companion.tag
override val visionSerializersModule: SerializersModule get() = controlsVisionSerializersModule actual override val visionSerializersModule: SerializersModule get() = controlsVisionSerializersModule
public actual companion object : PluginFactory<ControlVisionPlugin> { public actual companion object : PluginFactory<ControlVisionPlugin> {
override val tag: PluginTag = PluginTag("controls.vision") actual override val tag: PluginTag = PluginTag("controls.vision")
override fun build(context: Context, meta: Meta): ControlVisionPlugin = ControlVisionPlugin() actual override fun build(context: Context, meta: Meta): ControlVisionPlugin = ControlVisionPlugin()
} }
} }

View File

@ -2,7 +2,8 @@ import org.jetbrains.compose.ExperimentalComposeLibrary
plugins { plugins {
id("space.kscience.gradle.mpp") id("space.kscience.gradle.mpp")
alias(spclibs.plugins.compose) alias(spclibs.plugins.compose.compiler)
alias(spclibs.plugins.compose.jb)
`maven-publish` `maven-publish`
} }

View File

@ -2,8 +2,8 @@ package space.kscience.controls.compose
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowLeft import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material.icons.filled.KeyboardArrowRight import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -30,7 +30,7 @@ public fun NumberTextField(
Row (verticalAlignment = Alignment.CenterVertically, modifier = modifier) { Row (verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
step.takeIf { it > 0.0 }?.let { step.takeIf { it > 0.0 }?.let {
IconButton({ onValueChange(value.toDouble() - step) }, enabled = enabled) { IconButton({ onValueChange(value.toDouble() - step) }, enabled = enabled) {
Icon(Icons.Default.KeyboardArrowLeft, "decrease value") Icon(Icons.AutoMirrored.Filled.KeyboardArrowLeft, "decrease value")
} }
} }
TextField( TextField(
@ -52,7 +52,7 @@ public fun NumberTextField(
) )
step.takeIf { it > 0.0 }?.let { step.takeIf { it > 0.0 }?.let {
IconButton({ onValueChange(value.toDouble() + step) }, enabled = enabled) { IconButton({ onValueChange(value.toDouble() + step) }, enabled = enabled) {
Icon(Icons.Default.KeyboardArrowRight, "increase value") Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, "increase value")
} }
} }
} }

View File

@ -1,6 +1,7 @@
plugins { plugins {
kotlin("jvm") kotlin("jvm")
alias(spclibs.plugins.compose) alias(spclibs.plugins.compose.compiler)
alias(spclibs.plugins.compose.jb)
} }

View File

@ -3,7 +3,8 @@ import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
plugins { plugins {
id("space.kscience.gradle.mpp") id("space.kscience.gradle.mpp")
alias(spclibs.plugins.compose) alias(spclibs.plugins.compose.compiler)
alias(spclibs.plugins.compose.jb)
} }
kscience { kscience {

View File

@ -85,7 +85,7 @@ private class BodyOnSprings(
} }
fun main() = application { fun main() = application {
val initialState = XYZ<Meters>(0, 0.4, 0) val initialState = XYZ<Meters>(0.05, 0.4, 0)
Window(title = "Ball on springs", onCloseRequest = ::exitApplication) { Window(title = "Ball on springs", onCloseRequest = ::exitApplication) {
window.minimumSize = Dimension(400, 400) window.minimumSize = Dimension(400, 400)

View File

@ -32,10 +32,10 @@ import space.kscience.controls.constructor.devices.Drive
import space.kscience.controls.constructor.devices.LimitSwitch import space.kscience.controls.constructor.devices.LimitSwitch
import space.kscience.controls.constructor.devices.LinearDrive import space.kscience.controls.constructor.devices.LinearDrive
import space.kscience.controls.constructor.models.Inertia import space.kscience.controls.constructor.models.Inertia
import space.kscience.controls.constructor.models.Leadscrew
import space.kscience.controls.constructor.models.MutableRangeState import space.kscience.controls.constructor.models.MutableRangeState
import space.kscience.controls.constructor.models.PidParameters import space.kscience.controls.constructor.models.PidParameters
import space.kscience.controls.constructor.models.ScrewDrive import space.kscience.controls.constructor.onTimer
import space.kscience.controls.constructor.timer
import space.kscience.controls.constructor.units.Kilograms import space.kscience.controls.constructor.units.Kilograms
import space.kscience.controls.constructor.units.Meters import space.kscience.controls.constructor.units.Meters
import space.kscience.controls.constructor.units.NumericalValue import space.kscience.controls.constructor.units.NumericalValue
@ -56,13 +56,13 @@ import kotlin.time.DurationUnit
class Modulator( class Modulator(
context: Context, context: Context,
target: MutableDeviceState<NumericalValue<Meters>>, target: MutableDeviceState<NumericalValue<Meters>>,
var freq: Double = 0.1,
var timeStep: Duration = 5.milliseconds, var timeStep: Duration = 5.milliseconds,
var freq: Double = 0.1,
) : DeviceConstructor(context) { ) : DeviceConstructor(context) {
private val clockStart = clock.now() private val clockStart = clock.now()
private val modulation = timer(10.milliseconds).onNext { private val modulation = onTimer(timeStep) { _, next ->
val timeFromStart = clock.now() - clockStart val timeFromStart = next - clockStart
val t = timeFromStart.toDouble(DurationUnit.SECONDS) val t = timeFromStart.toDouble(DurationUnit.SECONDS)
target.value = NumericalValue( target.value = NumericalValue(
5 * sin(2.0 * PI * freq * t) + 5 * sin(2.0 * PI * freq * t) +
@ -72,9 +72,9 @@ class Modulator(
} }
private val mass = NumericalValue<Kilograms>(0.1) private val mass = NumericalValue<Kilograms>(1)
private val leverage = NumericalValue<Meters>(0.05) private val leverage = NumericalValue<Meters>(1.0)
private val maxAge = 10.seconds private val maxAge = 10.seconds
@ -95,7 +95,7 @@ internal fun createLinearDriveModel(
val drive = Drive(context) val drive = Drive(context)
//a screw drive to convert a rotational moment into a force //a screw drive to convert a rotational moment into a force
val screwDrive = ScrewDrive(context, leverage) val leadscrew = Leadscrew(context, leverage)
/** /**
@ -107,7 +107,7 @@ internal fun createLinearDriveModel(
*/ */
val inertiaModel = Inertia.linear( val inertiaModel = Inertia.linear(
context = context, context = context,
force = screwDrive.torqueToForce(drive.force), force = leadscrew.torqueToForce(drive.force),
mass = mass, mass = mass,
position = position position = position
) )
@ -130,6 +130,8 @@ private fun createModulator(linearDrive: LinearDrive): Modulator = linearDrive.c
Modulator(linearDrive.context, linearDrive.pid.target) Modulator(linearDrive.context, linearDrive.pid.target)
) )
private val startPid = PidParameters(kp = 250.0, ki = 0.0, kd = -20.0, timeStep = 20.milliseconds)
@OptIn(ExperimentalSplitPaneApi::class, ExperimentalKoalaPlotApi::class) @OptIn(ExperimentalSplitPaneApi::class, ExperimentalKoalaPlotApi::class)
fun main() = application { fun main() = application {
val context = remember { val context = remember {
@ -140,7 +142,7 @@ fun main() = application {
} }
var pidParameters by remember { var pidParameters by remember {
mutableStateOf(PidParameters(kp = 900.0, ki = 20.0, kd = -50.0, timeStep = 0.005.seconds)) mutableStateOf(startPid)
} }
val linearDrive: LinearDrive = remember { val linearDrive: LinearDrive = remember {
@ -183,14 +185,14 @@ fun main() = application {
NumberTextField( NumberTextField(
value = pidParameters.kp, value = pidParameters.kp,
onValueChange = { pidParameters = pidParameters.copy(kp = it.toDouble()) }, onValueChange = { pidParameters = pidParameters.copy(kp = it.toDouble()) },
formatter = { String.format("%.2f", it.toDouble()) }, formatter = { String.format("%.3f", it.toDouble()) },
step = 1.0, step = 0.01,
modifier = Modifier.width(200.dp), modifier = Modifier.width(200.dp),
) )
Slider( Slider(
pidParameters.kp.toFloat(), pidParameters.kp.toFloat(),
{ pidParameters = pidParameters.copy(kp = it.toDouble()) }, { pidParameters = pidParameters.copy(kp = it.toDouble()) },
valueRange = 0f..1000f, valueRange = 0f..100f,
steps = 100 steps = 100
) )
} }
@ -199,15 +201,15 @@ fun main() = application {
NumberTextField( NumberTextField(
value = pidParameters.ki, value = pidParameters.ki,
onValueChange = { pidParameters = pidParameters.copy(ki = it.toDouble()) }, onValueChange = { pidParameters = pidParameters.copy(ki = it.toDouble()) },
formatter = { String.format("%.2f", it.toDouble()) }, formatter = { String.format("%.3f", it.toDouble()) },
step = 0.1, step = 0.01,
modifier = Modifier.width(200.dp), modifier = Modifier.width(200.dp),
) )
Slider( Slider(
pidParameters.ki.toFloat(), pidParameters.ki.toFloat(),
{ pidParameters = pidParameters.copy(ki = it.toDouble()) }, { pidParameters = pidParameters.copy(ki = it.toDouble()) },
valueRange = -100f..100f, valueRange = -10f..10f,
steps = 100 steps = 100
) )
} }
@ -216,15 +218,15 @@ fun main() = application {
NumberTextField( NumberTextField(
value = pidParameters.kd, value = pidParameters.kd,
onValueChange = { pidParameters = pidParameters.copy(kd = it.toDouble()) }, onValueChange = { pidParameters = pidParameters.copy(kd = it.toDouble()) },
formatter = { String.format("%.2f", it.toDouble()) }, formatter = { String.format("%.3f", it.toDouble()) },
step = 0.1, step = 0.01,
modifier = Modifier.width(200.dp), modifier = Modifier.width(200.dp),
) )
Slider( Slider(
pidParameters.kd.toFloat(), pidParameters.kd.toFloat(),
{ pidParameters = pidParameters.copy(kd = it.toDouble()) }, { pidParameters = pidParameters.copy(kd = it.toDouble()) },
valueRange = -100f..100f, valueRange = -10f..10f,
steps = 100 steps = 100
) )
} }
@ -247,12 +249,7 @@ fun main() = application {
} }
Row { Row {
Button({ Button({
pidParameters = PidParameters( pidParameters = startPid
kp = 2.5,
ki = 0.0,
kd = -0.1,
timeStep = 0.005.seconds
)
}) { }) {
Text("Reset") Text("Reset")
} }

View File

@ -17,7 +17,7 @@ import kotlinx.coroutines.isActive
import space.kscience.controls.constructor.* import space.kscience.controls.constructor.*
import space.kscience.controls.constructor.devices.StepDrive import space.kscience.controls.constructor.devices.StepDrive
import space.kscience.controls.constructor.devices.angle import space.kscience.controls.constructor.devices.angle
import space.kscience.controls.constructor.models.ScrewDrive import space.kscience.controls.constructor.models.Leadscrew
import space.kscience.controls.constructor.models.coerceIn import space.kscience.controls.constructor.models.coerceIn
import space.kscience.controls.constructor.units.* import space.kscience.controls.constructor.units.*
import space.kscience.controls.manager.ClockManager import space.kscience.controls.manager.ClockManager
@ -99,11 +99,11 @@ private class PlotterModel(
) : ModelConstructor(context) { ) : ModelConstructor(context) {
private val xDrive = StepDrive(context, ticksPerSecond) private val xDrive = StepDrive(context, ticksPerSecond)
private val xTransmission = ScrewDrive(context, NumericalValue(0.01)) private val xTransmission = Leadscrew(context, NumericalValue(0.01))
val x = xTransmission.degreesToMeters(xDrive.angle(step)).coerceIn(xRange) val x = xTransmission.degreesToMeters(xDrive.angle(step)).coerceIn(xRange)
private val yDrive = StepDrive(context, ticksPerSecond) private val yDrive = StepDrive(context, ticksPerSecond)
private val yTransmission = ScrewDrive(context, NumericalValue(0.01)) private val yTransmission = Leadscrew(context, NumericalValue(0.01))
val y = yTransmission.degreesToMeters(yDrive.angle(step)).coerceIn(yRange) val y = yTransmission.degreesToMeters(yDrive.angle(step)).coerceIn(yRange)
val xy: DeviceState<XY<Meters>> = combineState(x, y) { x, y -> XY(x, y) } val xy: DeviceState<XY<Meters>> = combineState(x, y) { x, y -> XY(x, y) }
@ -143,7 +143,7 @@ suspend fun main() = application {
val range = -1000..1000 val range = -1000..1000
// plotter.modernArt(range, range) // plotterModel.plotter.modernArt(range, range)
plotterModel.plotter.square(range, range) plotterModel.plotter.square(range, range)
} }

View File

@ -0,0 +1,41 @@
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
plugins {
id("space.kscience.gradle.mpp")
alias(spclibs.plugins.compose.compiler)
alias(spclibs.plugins.compose.jb)
}
kscience {
jvm()
useSerialization()
useContextReceivers()
commonMain {
implementation(projects.controlsVisualisationCompose)
implementation(projects.controlsConstructor)
}
jvmMain {
// implementation("io.ktor:ktor-server-cio")
implementation(spclibs.logback.classic)
implementation(libs.sciprog.maps.compose)
}
}
kotlin {
sourceSets {
jvmMain {
dependencies {
implementation(compose.desktop.currentOs)
}
}
}
}
kotlin.explicitApi = ExplicitApiMode.Disabled
compose.desktop {
application {
mainClass = "space.kscience.controls.demo.map.MainKt"
}
}

View File

@ -0,0 +1,8 @@
package space.kscience.controls.demo.map
import androidx.compose.ui.window.application
fun main() = application {
}

View File

@ -1,6 +1,7 @@
plugins { plugins {
id("space.kscience.gradle.jvm") id("space.kscience.gradle.jvm")
alias(spclibs.plugins.compose) alias(spclibs.plugins.compose.compiler)
alias(spclibs.plugins.compose.jb)
} }
kotlin{ kotlin{

View File

@ -7,4 +7,4 @@ org.gradle.parallel=true
org.gradle.configureondemand=true org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx4096m org.gradle.jvmargs=-Xmx4096m
toolsVersion=0.15.2-kotlin-1.9.22 toolsVersion=0.15.4-kotlin-2.0.0

View File

@ -81,6 +81,8 @@ visionforge-markdown = { module = "space.kscience:visionforge-markdown", version
visionforge-server = { module = "space.kscience:visionforge-server", version.ref = "visionforge" } visionforge-server = { module = "space.kscience:visionforge-server", version.ref = "visionforge" }
visionforge-compose-html = { module = "space.kscience:visionforge-compose-html", version.ref = "visionforge" } visionforge-compose-html = { module = "space.kscience:visionforge-compose-html", version.ref = "visionforge" }
sciprog-maps-compose = "space.kscience:maps-kt-compose:0.3.0"
# Buildscript # Buildscript
[plugins] [plugins]

View File

@ -86,5 +86,6 @@ include(
":demo:motors", ":demo:motors",
":demo:echo", ":demo:echo",
":demo:mks-pdr900", ":demo:mks-pdr900",
":demo:constructor" ":demo:constructor",
":demo:devices-on-map"
) )