Add a FurutaSwingupJuliaC analysis: deploy the swing-up controller as a trimmed JuliaC binary - #8
Add a FurutaSwingupJuliaC analysis: deploy the swing-up controller as a trimmed JuliaC binary#8baggepinnen wants to merge 2 commits into
Conversation
A third target next to running in this process and exporting C: JuliaC compiles a program into a standalone executable with no Julia installation behind it. `compile_program_source` is `compile_program`'s sibling — both go through the new `program_signature`, so the model and the node signature are built once — and returns the generated declarations as Julia source instead of evaluating them. That is what `--trim` needs: the node has to be *defined in a package*, so that precompiling it compiles the node and serializes the executable into the package image (JuliaComputing/SynchJulia.jl#203). A module `eval`'d into SynchToolkit at runtime has no image to be serialized into. `export_program_juliac` writes that package and builds it. It mirrors `export_program_c` closely, because the deployed thing is the same: the node does its own I/O and its own logging, so the application around it is only timing — a Julia transcription of csrc/run_hardware.c, reading none of the node's outputs. Like the C export it carries the `csrc/` implementations along, builds them into its own `deps/`, and names them by absolute path, so an application belongs to the directory it was written into. Two things had to be handled that the C path does not have to: - The generated node refers to this library's operators through spliced function objects, which print qualified (`QuanserComponents.hw_measure`). Emitted verbatim, the application would depend on this package and pull the whole modelling stack into the binary. `compile_program_source` rewrites them to bare names and reports which it rewrote; `emit_program_ffi` then defines exactly those as `ccall`s over the copied `csrc/`. Base functions the node also calls print unqualified and resolve anywhere, hence a parent-module test rather than a name list. - `step!`/`reset!` are only statically resolvable with SynchJulia >= 0.4.3 (JuliaComputing/SynchJulia.jl#226) and its `dynamic_execution = false` preference, which the emitted Project.toml sets: the executable is built during precompilation and no definition can change afterwards, which is exactly the condition that preference documents. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PA77Eh76Yyro9eZXN9JuR1
The swing-up controller deployed as a statically compiled binary: same program, same designed gains and same log as `FurutaSwingupExperiment`, compiled by JuliaC instead of `make`. `export_swingup_juliac` is the plain entry point, `FurutaSwingupJuliaC` the analysis, with the emitted application, the JuliaC build log and the hardware run log as its artifacts. It is a separate analysis rather than another target on the existing one, but it still extends `FurutaSwingupBase`, so the parameter set every hardware run shares is declared once: what it adds is where the application goes and how it is compiled (`app_name`, `julia_channel`, `trim`, `build`). The fan-out in analysis_base.jl routes the generated entry point to its spec, which is what keeps one typed `run_analysis` per analysis; `app_name` and friends are what identify it there. The C and deploy parameters come along with the shared root and have no effect here, which the partial's docstring says. The test suite covers the emission unconditionally — including that the node's operator references really are local to the application, since a qualified one would quietly pull the modelling stack into the binary — and gates the build on `juliac_available()`: where Julia 1.13 and JuliaC are installed it builds with `--trim=safe`, checks the log for verifier errors, checks no C toolchain leaked into the bundle, and runs the binary. Verified here: 37 assertions, zero verifier errors, a 3.3 MB binary that starts, resolves its own libraries and reports the device this host has no SDK for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PA77Eh76Yyro9eZXN9JuR1
20f0173 to
99a3683
Compare
The generated node refers to our operators qualified, and what this branch does about itWorth recording, because it is the one thing in this branch that is a workaround rather than a What happens
julia> node = last(gen.decls);
julia> # every leaf that is one of ours, by type:
typeof(QuanserComponents.hw_measure)
typeof(QuanserComponents.hw_write)
typeof(QuanserComponents.log_row) # …and hw_shoulder, hw_elbow, hw_time,
# hw_dt, hw_exec, hw_count_shoulder,
# hw_count_elbowSo Note this is invisible on the C path: The fix hereTwo halves, both in this branch:
Net effect: the emitted application depends on SynchJulia, SynchCompiler, StaticArrays and Why it wants automating
Left as is for now, since it is small, checked by tests, and changes only when an operator is |
Adds a third target next to running a program in this process and exporting it as C: JuliaC compiles it into a standalone executable with no Julia installation behind it.
FurutaSwingupJuliaCis the analysis;export_swingup_juliacthe plain entry point.(Rebased onto the consolidated program layer — the earlier version of this branch predated it and is preserved in the
juliac-v1-backuptag. The port turned out smaller than what it replaced:resolve_tunablesand JuliaComputing/SynchToolkit.jl#150's generated constructors do what it used to do by hand, and since the node does its own I/O and logging, the application is only a timing loop.)Shape
compile_program_sourceiscompile_program's sibling — both go through the newprogram_signature, so the model and node signature are built once — and returns the generated declarations as Julia source instead of evaluating them. That is what--trimneeds: the node must be defined in a package, so precompiling it compiles the node and serializes the executable into the package image (JuliaComputing/SynchJulia.jl#203). A moduleeval'd into SynchToolkit at runtime has no image to be serialized into.export_program_juliacwrites that package and builds it, mirroringexport_program_c:The application is a Julia transcription of
csrc/run_hardware.cand reads none of the node's outputs, for the same reason that file does not: the program logs what it wants logged. Like the C export it carries thecsrc/implementations along and names them by absolute path, so an application belongs to the directory it was written into — deploying elsewhere means running the analysis there, which is what the C target'smakeamounts to too.Verification
37 assertions, including the gated end-to-end path:
--trim=safebuilds with zero verifier errors, noclang*anywhere in the bundle (as JuliaComputing/SynchJulia.jl's owntest/caching/trim.jlchecks), and the binary starts, resolves its own libraries and reports the device it cannot open — this host has no HIL SDK, soqube_hw_openfails by design. The remaining step is a run on the rig.Two things worth knowing
Operator references had to be localized. The generated node refers to our I/O operators through spliced function objects, which print qualified (
QuanserComponents.hw_measure); emitted verbatim the application would depend on this package and pull the modelling stack into the binary. They are rewritten to bare names and defined locally asccalls over the copiedcsrc/. Documented at length in a comment below, since the mapping table it needs looks derivable rather than independent.step!/reset!need two upstream pieces, both now available: SynchJulia ≥ 0.4.3 (JuliaComputing/SynchJulia.jl#226) and itsdynamic_execution = falsepreference, which the emitted Project.toml sets — legitimate here precisely because the executable is built during precompilation and no definition can change afterwards. Without them the trim verifier rejects six call sites; this was JuliaComputing/SynchJulia.jl#211.Analysis structure
A separate analysis rather than another target on
FurutaSwingupExperiment, but it extendsFurutaSwingupBase, so the parameter set every hardware run shares is declared once; what it adds isapp_name,julia_channel,trim,build. The fan-out inanalysis_base.jlroutes the generated entry point to its own spec andrun_analysis. The C and deploy parameters come along with the shared root and have no effect here, which the partial's docstring says.Environment note
This needs SynchToolkit from git
main, not registry 0.4.9:main's owngenerate_swingup_controllerfails on 0.4.9 withUntranslatable/unhandled term encountered: ModelingToolkitBase.SampleTime(nothing), which JuliaComputing/SynchToolkit.jl#172 fixed and no release carries yet. (Both copies can sit in the depot at once, sopathof(QuanserComponents.SynchToolkit)is worth checking when that error appears.) Unrelated: the current dyad CLI wants to drop a module qualification ingenerated/QubePendulum_definition.jl(MultibodyComponents.RootedFrame.FrameA()→RootedFrame.FrameA()); that churn is left out of this branch.🤖 Generated with Claude Code
https://claude.ai/code/session_01PA77Eh76Yyro9eZXN9JuR1