diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df0fb7a..a226a6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,6 +83,22 @@ jobs: run: julia --color=yes tests/julia/runtests.jl shell: bash + julia-docs: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: julia-actions/setup-julia@v3 + with: + version: '1.10' + + - name: Install documentation dependencies + run: julia --project=docs -e 'using Pkg; Pkg.instantiate()' + + - name: Build Julia documentation + run: julia --project=docs docs/make.jl + julia-matlab-verilog-test: runs-on: ubuntu-latest diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..14da2a4 --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,6 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" + +[compat] +Documenter = "1" +julia = "1.10" diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 0000000..ef81049 --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,18 @@ +using Documenter + +include(joinpath(@__DIR__, "..", "concore.jl")) +using .Concore + +makedocs(; + modules=[Concore], + sitename="Concore.jl", + checkdocs=:exports, + format=Documenter.HTML(; inventory_version="dev"), + pages=[ + "Home" => "index.md", + "Getting Started" => "getting-started.md", + "API Reference" => "api.md", + "Backends" => "backends.md", + "Wire Format" => "wire-format.md", + ], +) diff --git a/docs/src/api.md b/docs/src/api.md new file mode 100644 index 0000000..75f31d6 --- /dev/null +++ b/docs/src/api.md @@ -0,0 +1,48 @@ +# [API Reference](@id api-reference) + +```@meta +CurrentModule = Concore +``` + +## Protocol + +```@docs +concore_read +concore_write +initval +unchanged +``` + +## Configuration + +```@docs +safe_parse_list +tryparam +default_maxtime! +load_iport! +load_oport! +load_params! +concore_init! +``` + +The compatibility names `default_maxtime`, `load_iport`, `load_oport`, +`load_params`, and `concore_init` are aliases for the corresponding functions +above. + +`FileTransport`, `MmapTransport`, and `ZmqTransport` are aliases for the +corresponding backend types. + +## Backends + +```@docs +AbstractBackend +FileBackend +MmapBackend +mmap_cleanup +ZmqBackend +``` + +ZMQ ports are registered with +`init_zmq_port(name, mode, address, socket_type)` and closed with +`terminate_zmq()`. These functions require the optional ZMQ.jl package. See +[ZMQ](@ref zmq) for a complete call sequence. diff --git a/docs/src/backends.md b/docs/src/backends.md new file mode 100644 index 0000000..8250d60 --- /dev/null +++ b/docs/src/backends.md @@ -0,0 +1,102 @@ +# [Backends](@id backends) + +The active backend controls how `concore_read` and `concore_write` move the +same wire-format strings. File is the default backend. + +## File + +`FileBackend` uses ordinary files and has no external Julia dependencies. + +```julia +include("concore.jl") +using .Concore + +concore_init!(FileBackend()) +``` + +Local input and output path prefixes are `./in` and `./out`. The numeric port +is appended to the prefix, so port 1 uses `./in1` and `./out1`. + +## Mmap + +`MmapBackend` keeps the file-based naming and wire format but accesses each +file through a fixed-size memory-mapped segment. The default segment size is +4096 bytes. + +```julia +concore_init!(MmapBackend()) + +try + value = concore_read(1, "ym", "[0.0, 0.0]") + concore_write(1, "u", value) +finally + mmap_cleanup() +end +``` + +Pass a different segment size when constructing the backend if the wire value +does not fit in the default segment: + +```julia +concore_init!(MmapBackend(8192)) +``` + +## [ZMQ](@id zmq) + +ZMQ support is optional. Install ZMQ.jl in the active Julia environment before +including `concore.jl`: + +```julia +using Pkg +Pkg.add("ZMQ") +``` + +`Concore.HAS_ZMQ` reports whether the package was available when the runtime +was loaded. ZMQ ports use string names instead of numeric file ports. This +example shows the request side of a REQ/REP pair: + +```julia +include("concore.jl") +using .Concore + +concore_init!(ZmqBackend()) +init_zmq_port("req", "connect", "tcp://127.0.0.1:5555", "REQ") + +try + concore_write("req", "u", [1.0]) + ym = concore_read("req", "ym", "[0.0, 0.0]") +finally + terminate_zmq() +end +``` + +A REP peer must bind the same address and receive before replying. + +## Docker + +Docker is a runtime path variant, not a separate backend type. +`concoredocker.jl` uses `/in` and `/out` as its path prefixes, producing paths +such as `/in1/ym` and `/out1/u`. + +For generated Docker studies, `mkconcore.py` copies `concoredocker.jl` into the +node build directory as `concore.jl`. The node source therefore keeps the same +include statement used locally: + +```julia +include("concore.jl") +using .Concore +``` + +Build the mixed Julia controller and Python plant example with the existing +study generator: + +```sh +concore build demo/sampleJ.graphml --source demo --output docker-julia-demo --type docker --compose +cd docker-julia-demo +./build +./maxtime 5 +docker compose up +``` + +The generated Compose file mounts the connected input and output directories +at the paths expected by each container. diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md new file mode 100644 index 0000000..81ffe45 --- /dev/null +++ b/docs/src/getting-started.md @@ -0,0 +1,99 @@ +# [Getting Started](@id getting-started) + +```@meta +DocTestSetup = :(using Main.Concore) +``` + +## Setup + +Concore.jl requires Julia 1.10 or later. Clone the Concore repository and run +Julia from the repository root: + +```sh +git clone https://github.com/ControlCore-Project/concore.git +cd concore +julia +``` + +Load the standalone runtime from a Julia node: + +```julia +include("concore.jl") +using .Concore +``` + +There are no required Julia package dependencies for the default File backend. +ZMQ.jl is only needed when using the ZMQ backend. + +## Node loop + +A Concore node reads until its input changes, performs its calculation, and +writes its output. This controller reads `ym` from input port 1 and writes `u` +to output port 1: + +```julia +include("concore.jl") +using .Concore + +Concore.default_maxtime!(100) +Concore.delay = 0.02 + +ym = initval("[0.0, 0.0]") + +while Concore.simtime < Concore.maxtime + while unchanged() + ym = concore_read(1, "ym", "[0.0, 0.0]") + end + + u = 1.01 .* ym + concore_write(1, "u", u; delta=0) +end +``` + +With the default paths, port 1 reads from `./in1/ym` and writes to +`./out1/u`. The study runner creates and connects those directories. + +The repository also contains a mixed Python and Julia file-backend demo: + +```sh +julia demo/run_julia_mixed_demo.jl +``` + +## Initial values and simulation time + +`initval` parses a wire value and sets `Concore.simtime` from its first value: + +```jldoctest +julia> ym = initval("[0.0, 1.5]"); (Concore.simtime, ym) +(0.0, [1.5]) +``` + +`ym` is `[1.5]` and `Concore.simtime` is `0.0`. A read returns only the data +values and updates simulation time to the largest timestamp seen. A write uses +`Concore.simtime + delta` as the outgoing timestamp. + +## Configuration + +The runtime loads the same files as the other Concore implementations: + +- `concore.iport` maps named input ports to numbers. +- `concore.oport` maps named output ports to numbers. +- `concore.params` supplies values returned by `tryparam`. +- `concore.maxtime` sets the simulation limit used by `default_maxtime!`. + +Parameters can use Python dictionary syntax or semicolon-separated key/value +pairs: + +```text +{'gain': 1.5, 'mode': 'auto'} +``` + +```text +gain=1.5;mode=auto +``` + +Use a default when a parameter is absent: + +```julia +gain = tryparam("gain", 1.0) +``` diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..0dc7849 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,24 @@ +# Concore.jl + +Concore.jl is the native Julia implementation of the Concore protocol. It uses +the same wire format and synchronization pattern as the existing Concore +runtimes, without calling through Python. + +The runtime is the standalone file `concore.jl`. Julia nodes keep it next to +their source and include it directly: + +```julia +include("concore.jl") +using .Concore +``` + +File transport is the default. Memory-mapped files and ZeroMQ are available as +optional backend selections, and `concoredocker.jl` provides the path defaults +used by generated Docker studies. + +## Contents + +- [Getting Started](@ref getting-started) covers setup, the standard node loop, and configuration. +- [API Reference](@ref api-reference) lists the public Julia interface. +- [Backends](@ref backends) describes File, Mmap, ZMQ, and Docker usage. +- [Wire Format](@ref wire-format) documents message encoding and simulation time. diff --git a/docs/src/wire-format.md b/docs/src/wire-format.md new file mode 100644 index 0000000..971672c --- /dev/null +++ b/docs/src/wire-format.md @@ -0,0 +1,55 @@ +# [Wire Format](@id wire-format) + +```@meta +DocTestSetup = :(using Main.Concore) +``` + +Concore messages are text lists containing a simulation timestamp followed by +zero or more data values: + +```text +[simtime, value1, value2, ...] +``` + +For example: + +```text +[5.0, 1.5, -2.0] +``` + +The first value is the timestamp. `concore_read` updates `Concore.simtime` and +returns only `[1.5, -2.0]` to the node. + +## Writing + +For vector writes, the outgoing timestamp is `Concore.simtime + delta`: + +```julia +Concore.simtime = 5.0 +concore_write(1, "u", [1.5, -2.0]; delta=1) +``` + +This writes: + +```text +[6.0, 1.5, -2.0] +``` + +Integer-valued finite floats are written with a `.0` suffix. Other values are +rounded to 15 significant digits to keep output consistent with the existing +Concore wire format. + +## Reading + +`safe_parse_list` parses wire values without calling `eval` or `Meta.parse`. +For compatibility with Python and NumPy output, it also accepts wrapped values +and Python literals: + +```jldoctest +julia> safe_parse_list("[0.0, np.float64(1.5), True, None]") == [0.0, 1.5, 1.0, 0.0] +true +``` + +Malformed input raises `ArgumentError`. The protocol, wire compatibility, and +interop tests cover Julia exchanges with the existing Python, C++, MATLAB, and +Verilog implementations.