Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ SynchJulia = "a1b2c3d4-5e6f-7a8b-9c0d-e1f2a3b4c5d6"
SynchToolkit = "f500ffa8-9682-42ac-8d34-0ca7926c2e94"
ThermalComponents = "084a29d9-4f48-4925-95b4-b7d4c79725f4"
TranslationalComponents = "42acc370-819b-4877-bdd6-eb3294461f5d"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

[compat]
ControlSystemsBase = "1"
Expand Down
33 changes: 33 additions & 0 deletions dyad/partial_swingup_juliac.dyad
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# The partial base lives in its own file, apart from the concrete analysis that extends it,
# for the reason spelled out in partial_swingup_experiment.dyad: a GUI edit to a concrete
# analysis rewrites its file and drops `partial` from any partial analysis sharing it
# (JuliaComputing/dyad-lang#1762).
"""
Deploy the swing-up controller as a statically compiled Julia binary.

Same program and same designed gains as `FurutaSwingupExperiment`, compiled by a different
route: instead of writing C to be built with `make`, this writes a small Julia application
package -- the code-generated node as source, plus a timing loop around it -- and compiles it
with JuliaC's `--trim=safe` into a standalone executable with no Julia installation behind it.
See `QuanserComponents.export_program_juliac`.

`build = false` stops after writing the application, which is how the generated sources are
inspected without a toolchain; `run = true` then drives the pendulum with the built binary.

The application's libraries are named by absolute path, so it belongs to the directory it was
written into -- deploying elsewhere means running this analysis there. The parameters
`QubeHardwareRunBase` contributes for the C and deploy targets (`export_c`, `backend`,
`deploy_host`, `live_plot`, ...) have no effect here.
"""
partial analysis FurutaSwingupJuliaCBase
extends FurutaSwingupBase(Tf = 10.0, output_dir = "furuta_juliac")
"Name of the generated application package and of the compiled executable"
parameter app_name::String = "FurutaSwingupApp"
"juliaup channel used for the build; JuliaC's --trim needs Julia 1.13 or newer"
parameter julia_channel::String = "1.13"
"JuliaC trimming mode: safe, unsafe, unsafe-warn or no"
parameter trim::String = "safe"
"Compile the generated application into a standalone binary; false only writes it"
parameter build::Boolean = true
relations
end
5 changes: 5 additions & 0 deletions dyad/swingup_juliac.dyad
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
analysis FurutaSwingupJuliaC
extends FurutaSwingupJuliaCBase()
model = FurutaHardware()
relations
end
96 changes: 96 additions & 0 deletions generated/FurutaSwingupJuliaC_definition.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
### DO NOT EDIT THIS FILE
### This file is auto-generated by the Dyad command-line compiler.
### If you edit this code it is likely to get overwritten.
### Instead, update the Dyad source code and regenerate this file


using DyadInterface
using DyadInterface: ODEAlg, DEVerbosity, OptimizationLevel
using ModelingToolkit: SymbolicT, toggle_namespacing
using QuanserComponents: AbstractQubeHardwareRunBaseSpec, QubeHardwareRunBaseSpec
@kwdef mutable struct FurutaSwingupJuliaCSpec <: AbstractQubeHardwareRunBaseSpec
name::Symbol = :FurutaSwingupJuliaC
# Sample time [s]
var"Ts"::Float64 = 0.005
# Run the program on the hardware; false only builds it
var"run"::Bool = false
# Duration [s] of the run; 0 means the program's own natural length
var"Tf"::Float64 = 10.0
# Motor saturation [V]
var"umax"::Float64 = 10.0
# Arm angle [deg] at start-up, added to every shoulder reading, so the arm need not be at
# its home position first
var"arm_deg"::Float64 = 0.0
# Card-specific options applied after opening the board, e.g.
# \"deadband_compensation=0.65\". Empty leaves qube_hw.c on its own default, which is
# what every run should normally use: the command-to-torque path has to be the same one
# the identified parameters were fitted against
var"card_options"::String = ""
# Backend to build the program on when it runs in-process, \"julia\" or \"c\". Ignored when
# `export_c` is true, which always builds C
var"backend"::String = "julia"
# Export the program as standalone C into `output_dir`, and run that instead of in-process
var"export_c"::Bool = true
# Directory the generated C sources and the log are written to
var"output_dir"::String = "furuta_juliac"
# File the program writes its log to. Passed to the `DataLogger` inside the model *and* used
# to open the file, so the two cannot disagree. Empty keeps the program's own default name.
# Exported and deployed runs use the bare file name inside their own directory, since an
# absolute path from this machine means nothing on the target
var"log_file"::String = ""
# Host to build and run on, e.g. \"username@hostname\"; empty runs on this machine. Implies
# `export_c`, since C sources are what gets copied over
var"deploy_host"::String = ""
# Directory on `deploy_host` to copy the sources into
var"deploy_dir"::String = "furuta_c"
# Launch a live plotter alongside the run to watch it as it happens (needs `run = true`)
var"live_plot"::Bool = false
# Live-plot viewer executable
var"live_plot_cmd"::String = "kst2"
# Viewer session file; a relative path resolves against `output_dir`
var"live_plot_config"::String = "kst2config.kst"
# LQR state-penalty diagonal in the order [shoulder_angle, elbow_angle, shoulder_velocity, elbow_velocity]
var"Q1"::Array{Float64, 1} = [1000.0, 10.0, 1.0, 1.0]
# LQR control-penalty weight
var"Q2"::Float64 = 100.0
# Name of the generated application package and of the compiled executable
var"app_name"::String = "FurutaSwingupApp"
# juliaup channel used for the build; JuliaC's --trim needs Julia 1.13 or newer
var"julia_channel"::String = "1.13"
# JuliaC trimming mode: safe, unsafe, unsafe-warn or no
var"trim"::String = "safe"
# Compile the generated application into a standalone binary; false only writes it
var"build"::Bool = true
# The swing-up controller closed around the physical QUBE, with the hardware I/O
# inside the synchronous program.
#
# `FurutaSwingup` is the same controller closed around the simulated
# `QubePendulum`; here the plant is replaced by `HardwareMeasurement` and
# `HardwareCommand`, which perform the encoder read and the amplifier write
# themselves. Compiling this model therefore yields a synchronous program that
# needs nothing from its caller but a clock tick -- see
# `generate_swingup_controller` and `SwingupController` in `src/codegen.jl`.
#
# The run log is written from inside the program too, by the `DataLogger`, in the
# `SWINGUP_LOG_COLUMNS` order: what was measured and applied, plus the loop
# diagnostics `HardwareDiagnostics` reports. So every target logs the same file
# through the same code — the C harness around the exported program is timing and
# nothing else, and a Julia driver adds no logging of its own either. The driver
# opens the file, since a filename cannot cross a synchronous node's interface;
# `log_file` is the name it must use, and `open_log!` is called with it
# automatically by the runners in src/program.jl.
var"model"::Union{Nothing, System} = QuanserComponents.FurutaHardware(; name=:FurutaHardware)
end

function DyadInterface.run_analysis(spec::FurutaSwingupJuliaCSpec)
overrides = Dict{SymbolicT, SymbolicT}()
no_namespace_model = toggle_namespacing(spec.model, false)

base_spec = QubeHardwareRunBaseSpec(;
name=:QubeHardwareRunBase, overrides, Ts=spec.Ts, run=spec.run, Tf=spec.Tf, umax=spec.umax, arm_deg=spec.arm_deg, card_options=spec.card_options, backend=spec.backend, export_c=spec.export_c, output_dir=spec.output_dir, log_file=spec.log_file, deploy_host=spec.deploy_host, deploy_dir=spec.deploy_dir, live_plot=spec.live_plot, live_plot_cmd=spec.live_plot_cmd, live_plot_config=spec.live_plot_config, Q1=spec.Q1, Q2=spec.Q2, app_name=spec.app_name, julia_channel=spec.julia_channel, trim=spec.trim, build=spec.build, model=spec.model
)
run_analysis(base_spec)
end

FurutaSwingupJuliaC(;kwargs...) = run_analysis(FurutaSwingupJuliaCSpec(;kwargs...))
export FurutaSwingupJuliaC, FurutaSwingupJuliaCSpec
5 changes: 1 addition & 4 deletions generated/definitions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,10 @@ end

import BlockComponents
import DiscreteComponents
import DyadData
import DyadInterface
import ElectricalComponents
import HydraulicComponents
import MultibodyComponents
import RotationalComponents
import ThermalComponents
import TranslationalComponents
@doc Markdown.doc"""
This connector represents an electrical pin with voltage and current as the potential and flow variables, respectively.
"""
Expand Down Expand Up @@ -219,6 +215,7 @@ include("FurutaHardware_definition.jl")
include("FurutaIdentificationExperiment_definition.jl")
include("FurutaIdentification_definition.jl")
include("FurutaSwingupExperiment_definition.jl")
include("FurutaSwingupJuliaC_definition.jl")
include("FurutaSwingup_definition.jl")
include("GoHome_definition.jl")
include("HardwareCommand_definition.jl")
Expand Down
9 changes: 6 additions & 3 deletions src/QuanserComponents.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,20 @@ include("traj_source.jl")
include("../generated/module.jl")

# Building a synchronous program for the rig and running it, in three layers: what every
# program shares (program.jl), how one gets onto hardware (harness.jl), and the programs
# themselves (codegen.jl for the swing-up controller, friction.jl for the friction experiment,
# identification.jl for the open-loop replay).
# program shares (program.jl), how one gets onto hardware (harness.jl for the C target,
# juliac.jl for a statically compiled Julia binary), and the programs themselves (codegen.jl
# for the swing-up controller, friction.jl for the friction experiment, identification.jl for
# the open-loop replay).
include("program.jl")
include("harness.jl")
include("juliac.jl")
include("codegen.jl")
include("friction.jl")
include("identification.jl")

# The analyses on top of them.
include("swingup_analysis.jl")
include("juliac_analysis.jl")
include("friction_analysis.jl")
include("identification_analysis.jl")

Expand Down
19 changes: 17 additions & 2 deletions src/analysis_base.jl
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,21 @@ end
Q2::Float64 = 100.0
end

# The JuliaC target's own analysis. It extends the swing-up base in Dyad, so it inherits the
# LQR weights and builds the same program; what is its own is where the application goes and
# how it is compiled. The C/deploy parameters come along with the shared root and are unused
# here, which is the price of one root for the whole rig.
@qube_run_spec FurutaSwingupJuliaCBaseSpec :FurutaSwingupJuliaCBase begin
Tf::Float64 = 10.0
output_dir::String = "furuta_juliac"
Q1::Vector{Float64} = [1000.0, 10.0, 1.0, 1.0]
Q2::Float64 = 100.0
app_name::String = "FurutaSwingupApp"
julia_channel::String = "1.13"
trim::String = "safe"
build::Bool = true
end

@qube_run_spec FurutaFrictionBaseSpec :FurutaFrictionBase begin
run::Bool = true
output_dir::String = "friction_c"
Expand Down Expand Up @@ -116,8 +131,8 @@ end
end

# Base specs an analysis' generated entry point may be asking for, most specific first.
const QUBE_RUN_SPECS = (FurutaSwingupBaseSpec, FurutaFrictionBaseSpec,
FurutaIdentificationBaseSpec)
const QUBE_RUN_SPECS = (FurutaSwingupBaseSpec, FurutaSwingupJuliaCBaseSpec,
FurutaFrictionBaseSpec, FurutaIdentificationBaseSpec)

"""
QubeHardwareRunBaseSpec(; parameters...)
Expand Down
31 changes: 29 additions & 2 deletions src/codegen.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ using ControlSystemsMTK: named_ss
using ControlSystemsBase: c2d, ss, lqr
using LinearAlgebra: Diagonal, I, pinv

export generate_swingup_controller, export_swingup_c, SwingupController, design_lqr,
swingup_log
export generate_swingup_controller, export_swingup_c, export_swingup_juliac,
SwingupController, design_lqr, swingup_log

"""
design_lqr(; Ts=0.005, Q1=[1000.0, 10.0, 1.0, 1.0], Q2=100.0) -> L::Vector{Float64}
Expand Down Expand Up @@ -187,3 +187,30 @@ function export_swingup_c(dir; Ts = 0.005, log_file = SWINGUP_LOG_FILE, L = noth
r = export_program_c(gen, dir; Tf, arm_deg, card_options, gains = gen.gains)
return (; r..., gen.compiled)
end

"""
export_swingup_juliac(dir; Ts=0.005, log_file=SWINGUP_LOG_FILE, L=nothing, umax=nothing,
Tf=10.0, arm_deg=0.0, card_options=nothing, app_name="FurutaSwingupApp",
julia_channel="1.13", trim="safe", build=true, overrides...)

Code-generate the swing-up controller as Julia source and export it as a standalone
application under `dir`, compiled into a trimmed binary unless `build = false` — see
[`export_program_juliac`](@ref), which does the work and documents what lands there. The
JuliaC counterpart of [`export_swingup_c`](@ref), built from the same model, the same designed
gains and the same log.

Returns what `export_program_juliac` returns.
"""
function export_swingup_juliac(dir; Ts = 0.005, log_file = SWINGUP_LOG_FILE, L = nothing,
umax = nothing, Tf = 10.0, arm_deg = 0.0,
card_options = nothing,
app_name::AbstractString = "FurutaSwingupApp",
julia_channel::AbstractString = APP_COMPAT.julia,
trim::AbstractString = "safe", build::Bool = true,
param_overrides = nothing, overrides...)
gen = compile_program_source(FurutaHardware; name = :controller, Ts,
tunables = SWINGUP_TUNABLES, outputs = _swingup_outputs,
log = swingup_log(log_file), param_overrides, overrides...)
return export_program_juliac(gen, dir; app_name, Tf, arm_deg, card_options,
gains = (; L, umax), julia_channel, trim, build)
end
Loading