diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index fb8467f..4cdc283 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -2,56 +2,103 @@ name: CI on: pull_request: push: - branches: - - master - tags: '*' + branches: [master] + tags: ['*'] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + # OM's Project.toml lives in the OM.jl subdir of the workspace; pin it so the + # cache action and inline julia calls key off the same project. + JULIA_PROJECT: OM.jl + jobs: test: - name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + name: Julia ${{ matrix.version }} - ${{ matrix.os }} runs-on: ${{ matrix.os }} + timeout-minutes: 120 strategy: - fail-fast: true + fail-fast: false matrix: - include: - - os: windows-latest - arch: 'x64' - version: '1.7' - - os: ubuntu-latest - arch: 'x64' - version: '1.7' - - os: macos-latest - arch: 'x64' - version: '1.7' + version: ['1.12'] + os: [ubuntu-latest, windows-latest] + arch: [x64] steps: - - uses: actions/checkout@v2 + # OM and every sub-package are checked out as SIBLINGS in the workspace. + # The packages' [sources] reference ../.jl, and we Pkg.develop them + # explicitly below. This mirrors OMFrontend.jl's working CI and avoids the + # submodule-pointer + nested-path issues entirely. + - name: Checkout OM.jl + uses: actions/checkout@v4 + with: + path: OM.jl + - { name: Checkout Absyn.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/Absyn.jl, ref: master, path: Absyn.jl } } + - { name: Checkout ArrayUtil.jl, uses: actions/checkout@v4, with: { repository: JKRT/ArrayUtil.jl, ref: master, path: ArrayUtil.jl } } + - { name: Checkout DAE.jl, uses: actions/checkout@v4, with: { repository: JKRT/DAE.jl, ref: master, path: DAE.jl } } + - { name: Checkout DoubleEnded.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/DoubleEnded.jl, ref: master, path: DoubleEnded.jl } } + - { name: Checkout ImmutableList.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/ImmutableList.jl, ref: master, path: ImmutableList.jl } } + - { name: Checkout ListUtil.jl, uses: actions/checkout@v4, with: { repository: JKRT/ListUtil.jl, ref: master, path: ListUtil.jl } } + - { name: Checkout MetaModelica.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/MetaModelica.jl, ref: master, path: MetaModelica.jl } } + - { name: Checkout OMBackend.jl, uses: actions/checkout@v4, with: { repository: JKRT/OMBackend.jl, ref: master, path: OMBackend.jl } } + - { name: Checkout OMFrontend.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/OMFrontend.jl, ref: master, path: OMFrontend.jl } } + - { name: Checkout OMParser.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/OMParser.jl, ref: master, path: OMParser.jl } } + - { name: Checkout OMRuntimeExternalC.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/OMRuntimeExternalC.jl, ref: master, path: OMRuntimeExternalC.jl } } + - { name: Checkout SCode.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/SCode.jl, ref: master, path: SCode.jl } } + + # libSimulationRuntimeC.so in the OMRuntimeExternalC.jl libs-v0.1.0 + # Linux zip has DT_NEEDED entries for liblapack.so.3, libblas.so.3 and + # libgfortran.so.5. ubuntu-24.04 does not ship those by default, so + # dlopen fails and every MSL / Spice3 / external-builtin test that goes + # through structural_simplify chains down to it. The Windows + # x86_64-mingw32 zip ships its own MinGW DLLs (libgfortran-5.dll, + # libopenblas.dll, ...) so no install step is needed there. + - name: Install OMC runtime system deps (Linux) + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends liblapack3 libblas3 libgfortran5 - - uses: julia-actions/setup-julia@v1 + - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/cache@v1 - env: - cache-name: cache-artifacts + - uses: julia-actions/cache@v2 with: - path: ~/.julia/artifacts - key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} - restore-keys: | - ${{ runner.os }}-test-${{ env.cache-name }}- - ${{ runner.os }}-test- - ${{ runner.os }}- - - name: Build - # Use bash always to avoid having to escape quotes for Windows + cache-name: om-ci + + - name: Develop siblings, build native libs, precompile shell: bash - run: julia --compiled-modules=no --color=yes --project -e 'import Pkg; Pkg.Registry.add("General"); Pkg.Registry.add(Pkg.RegistrySpec(url="https://github.com/OpenModelica/OpenModelicaRegistry.git")); Pkg.build(;verbose = true);Pkg.instantiate()' + working-directory: OM.jl + # Disable auto-precompile so the native-lib build runs before precompile + # (OMParser/OMRuntimeExternalC download shared libs in their build step). + # Build only those two: OMBackend/OMFrontend build.jl are standalone-dev + # scripts that Pkg.add deps from git and clobber the dev'd path sources. + env: + JULIA_PKG_PRECOMPILE_AUTO: '0' + run: | + julia --color=yes --project -e ' + import Pkg + Pkg.Registry.add("General") + Pkg.Registry.add(Pkg.RegistrySpec(url="https://github.com/OpenModelica/OpenModelicaRegistry.git")) + siblings = [ + "../Absyn.jl", "../ArrayUtil.jl", "../DAE.jl", "../DoubleEnded.jl", + "../ImmutableList.jl", "../ListUtil.jl", "../MetaModelica.jl", + "../OMBackend.jl", "../OMFrontend.jl", "../OMParser.jl", + "../OMRuntimeExternalC.jl", "../SCode.jl", + ] + Pkg.develop([Pkg.PackageSpec(path = p) for p in siblings]) + # Test-only deps used by test/testUtils.jl but absent from OM [deps] + # (already in the resolved manifest transitively; promote to direct). + Pkg.add(["ADTypes", "Sundials", "Logging", "Test"]) + Pkg.build(["OMParser", "OMRuntimeExternalC"]; verbose=true) + Pkg.resolve() + Pkg.precompile()' - name: Test - uses: julia-actions/julia-runtest@v1 - - - name: Calculate process coverage - uses: julia-actions/julia-processcoverage@v1 - - - name: Coverage report - uses: codecov/codecov-action@v1 - with: - file: lcov.info + shell: bash + working-directory: OM.jl/test # runtests.jl guards pwd() == @__DIR__ + run: julia --color=yes --project=.. -e 'include("runtests.jl")' diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..70ae5c2 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,75 @@ +name: Documentation +on: + push: + branches: [master] + tags: ['*'] + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + statuses: write + +jobs: + docs: + name: Build and deploy docs + runs-on: ubuntu-latest + timeout-minutes: 120 + steps: + - name: Checkout OM.jl + uses: actions/checkout@v4 + with: + path: OM.jl + - { name: Checkout Absyn.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/Absyn.jl, ref: master, path: Absyn.jl } } + - { name: Checkout ArrayUtil.jl, uses: actions/checkout@v4, with: { repository: JKRT/ArrayUtil.jl, ref: master, path: ArrayUtil.jl } } + - { name: Checkout DAE.jl, uses: actions/checkout@v4, with: { repository: JKRT/DAE.jl, ref: master, path: DAE.jl } } + - { name: Checkout DoubleEnded.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/DoubleEnded.jl, ref: master, path: DoubleEnded.jl } } + - { name: Checkout ImmutableList.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/ImmutableList.jl, ref: master, path: ImmutableList.jl } } + - { name: Checkout ListUtil.jl, uses: actions/checkout@v4, with: { repository: JKRT/ListUtil.jl, ref: master, path: ListUtil.jl } } + - { name: Checkout MetaModelica.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/MetaModelica.jl, ref: master, path: MetaModelica.jl } } + - { name: Checkout OMBackend.jl, uses: actions/checkout@v4, with: { repository: JKRT/OMBackend.jl, ref: master, path: OMBackend.jl } } + - { name: Checkout OMFrontend.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/OMFrontend.jl, ref: master, path: OMFrontend.jl } } + - { name: Checkout OMParser.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/OMParser.jl, ref: master, path: OMParser.jl } } + - { name: Checkout OMRuntimeExternalC.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/OMRuntimeExternalC.jl, ref: master, path: OMRuntimeExternalC.jl } } + - { name: Checkout SCode.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/SCode.jl, ref: master, path: SCode.jl } } + + - uses: julia-actions/setup-julia@v2 + with: + version: '1.12' + + - uses: julia-actions/cache@v2 + with: + cache-name: om-docs + + - name: Instantiate docs env (sibling develop + native build) + shell: bash + working-directory: OM.jl + env: + JULIA_PKG_PRECOMPILE_AUTO: '0' + run: | + julia --project=docs --color=yes -e ' + import Pkg + Pkg.Registry.add("General") + Pkg.Registry.add(Pkg.RegistrySpec(url="https://github.com/OpenModelica/OpenModelicaRegistry.git")) + siblings = [ + "../Absyn.jl", "../ArrayUtil.jl", "../DAE.jl", "../DoubleEnded.jl", + "../ImmutableList.jl", "../ListUtil.jl", "../MetaModelica.jl", + "../OMBackend.jl", "../OMFrontend.jl", "../OMParser.jl", + "../OMRuntimeExternalC.jl", "../SCode.jl", + ] + Pkg.develop([Pkg.PackageSpec(path = p) for p in siblings]) + Pkg.develop(Pkg.PackageSpec(path = ".")) + Pkg.build(["OMParser", "OMRuntimeExternalC"]; verbose=true) + Pkg.resolve() + Pkg.precompile()' + + - name: Build and deploy + shell: bash + working-directory: OM.jl + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} + run: julia --project=docs --color=yes docs/make.jl diff --git a/.github/workflows/julia-nightly.yml b/.github/workflows/julia-nightly.yml new file mode 100644 index 0000000..5903282 --- /dev/null +++ b/.github/workflows/julia-nightly.yml @@ -0,0 +1,94 @@ +name: Julia nightly +# Forward-guard against the next Julia. Not part of PR gating: weekly + +# manual dispatch only, so a break here never blocks a merge. +on: + workflow_dispatch: + schedule: + - cron: '11 7 * * 1' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + # OM's Project.toml lives in the OM.jl subdir of the workspace; pin it so the + # cache action and inline julia calls key off the same project. + JULIA_PROJECT: OM.jl + +jobs: + test-nightly: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} + runs-on: ${{ matrix.os }} + timeout-minutes: 120 + strategy: + fail-fast: false + matrix: + version: ['nightly'] + os: [ubuntu-latest, windows-latest] + arch: [x64] + steps: + # OM and every sub-package are checked out as SIBLINGS in the workspace. + # The packages' [sources] reference ../.jl, and we Pkg.develop them + # explicitly below. This mirrors OMFrontend.jl's working CI and avoids the + # submodule-pointer + nested-path issues entirely. + - name: Checkout OM.jl + uses: actions/checkout@v4 + with: + path: OM.jl + - { name: Checkout Absyn.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/Absyn.jl, ref: master, path: Absyn.jl } } + - { name: Checkout ArrayUtil.jl, uses: actions/checkout@v4, with: { repository: JKRT/ArrayUtil.jl, ref: master, path: ArrayUtil.jl } } + - { name: Checkout DAE.jl, uses: actions/checkout@v4, with: { repository: JKRT/DAE.jl, ref: master, path: DAE.jl } } + - { name: Checkout DoubleEnded.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/DoubleEnded.jl, ref: master, path: DoubleEnded.jl } } + - { name: Checkout ImmutableList.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/ImmutableList.jl, ref: master, path: ImmutableList.jl } } + - { name: Checkout ListUtil.jl, uses: actions/checkout@v4, with: { repository: JKRT/ListUtil.jl, ref: master, path: ListUtil.jl } } + - { name: Checkout MetaModelica.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/MetaModelica.jl, ref: master, path: MetaModelica.jl } } + - { name: Checkout OMBackend.jl, uses: actions/checkout@v4, with: { repository: JKRT/OMBackend.jl, ref: master, path: OMBackend.jl } } + - { name: Checkout OMFrontend.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/OMFrontend.jl, ref: master, path: OMFrontend.jl } } + - { name: Checkout OMParser.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/OMParser.jl, ref: master, path: OMParser.jl } } + - { name: Checkout OMRuntimeExternalC.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/OMRuntimeExternalC.jl, ref: master, path: OMRuntimeExternalC.jl } } + - { name: Checkout SCode.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/SCode.jl, ref: master, path: SCode.jl } } + + - uses: julia-actions/setup-julia@v2 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + + - uses: julia-actions/cache@v2 + with: + cache-name: om-nightly + + - name: Develop siblings, build native libs, precompile + shell: bash + working-directory: OM.jl + # Disable auto-precompile so the native-lib build runs before precompile + # (OMParser/OMRuntimeExternalC download shared libs in their build step). + # Build only those two: OMBackend/OMFrontend build.jl are standalone-dev + # scripts that Pkg.add deps from git and clobber the dev'd path sources. + env: + JULIA_PKG_PRECOMPILE_AUTO: '0' + run: | + julia --color=yes --project -e ' + import Pkg + Pkg.Registry.add("General") + Pkg.Registry.add(Pkg.RegistrySpec(url="https://github.com/OpenModelica/OpenModelicaRegistry.git")) + siblings = [ + "../Absyn.jl", "../ArrayUtil.jl", "../DAE.jl", "../DoubleEnded.jl", + "../ImmutableList.jl", "../ListUtil.jl", "../MetaModelica.jl", + "../OMBackend.jl", "../OMFrontend.jl", "../OMParser.jl", + "../OMRuntimeExternalC.jl", "../SCode.jl", + ] + Pkg.develop([Pkg.PackageSpec(path = p) for p in siblings]) + # Test-only deps used by test/testUtils.jl but absent from OM [deps] + # (already in the resolved manifest transitively; promote to direct). + Pkg.add(["ADTypes", "Sundials", "Logging", "Test"]) + Pkg.build(["OMParser", "OMRuntimeExternalC"]; verbose=true) + Pkg.resolve() + Pkg.precompile()' + + - name: Test + shell: bash + working-directory: OM.jl/test # runtests.jl guards pwd() == @__DIR__ + run: julia --color=yes --project=.. -e 'include("runtests.jl")' diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..a8b459b --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,9 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +OM = "2f925a0b-2436-437b-858e-49aee461894b" + +[sources] +OM = {path = ".."} + +[compat] +Documenter = "1" diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 0000000..3bd870b --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,27 @@ +using Documenter +using OM + +makedocs( + sitename = "OM.jl", + authors = "John Tinnerholm and contributors", + modules = [OM], + format = Documenter.HTML( + prettyurls = get(ENV, "CI", "false") == "true", + canonical = "https://JKRT.github.io/OM.jl", + ), + pages = [ + "Home" => "index.md", + "Installation" => "installation.md", + "Examples" => "examples.md", + "API reference" => "api.md", + ], + # Keep the first deploys from hard-failing on as-yet-undocumented bindings + # or cross-reference gaps; tighten to `false` once the API pages are filled. + warnonly = true, +) + +deploydocs( + repo = "github.com/JKRT/OM.jl.git", + devbranch = "master", + push_preview = true, +) diff --git a/docs/src/api.md b/docs/src/api.md new file mode 100644 index 0000000..e1b3be3 --- /dev/null +++ b/docs/src/api.md @@ -0,0 +1,32 @@ +# API reference + +The public API is called through the `OM` module (functions are qualified, +e.g. `OM.simulate`). + +## Simulation and translation + +```@docs +OM.simulate +OM.translate +OM.flatten +OM.getMTKProblem +OM.resimulate +``` + +## Libraries + +```@docs +OM.loadLibrary +OM.loadPackage +OM.loadInstalledLibrary +OM.installLibrary +OM.libraries +``` + +## Output and inspection + +```@docs +OM.exportCSV +OM.writeModelToFile +OM.exportModelica +``` diff --git a/docs/src/examples.md b/docs/src/examples.md new file mode 100644 index 0000000..11fbd15 --- /dev/null +++ b/docs/src/examples.md @@ -0,0 +1,32 @@ +# Examples + +## A simple pendulum + +```julia +using OM +sol = OM.simulate("Modelica.Mechanics.MultiBody.Examples.Elementary.Pendulum"; + MSL_Version = "MSL:3.2.3", stopTime = 1.0) +``` + +## Translate without simulating + +Build the in-memory model (frontend + backend) and inspect or solve it later: + +```julia +OM.translate("Modelica.Electrical.Analog.Examples.DifferenceAmplifier"; + MSL_Version = "MSL:3.2.3") +prob = OM.getMTKProblem("Modelica.Electrical.Analog.Examples.DifferenceAmplifier") +``` + +## Flatten only (frontend) + +```julia +(flatModel, functions) = OM.flatten("Modelica.Mechanics.MultiBody.Examples.Elementary.Pendulum") +``` + +## Export results + +```julia +sol = OM.simulate("Modelica....SomeModel"; MSL_Version = "MSL:3.2.3", stopTime = 1.0) +OM.exportCSV(sol, "result.csv") +``` diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..c749815 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,22 @@ +# OM.jl + +OM.jl is a Modelica compiler written in Julia. It takes Modelica models +(including the Modelica Standard Library), flattens and lowers them through a +Julia frontend and backend, and simulates them via the +[SciML](https://sciml.ai) / ModelingToolkit ecosystem. + +The typical user flow is a single call: + +```julia +using OM +sol = OM.simulate("Modelica.Mechanics.MultiBody.Examples.Elementary.Pendulum"; + MSL_Version = "MSL:3.2.3", stopTime = 1.0) +``` + +- **[Installation](installation.md)** — how to get OM.jl and its submodules set up. +- **[Examples](examples.md)** — worked simulations. +- **[API reference](api.md)** — the public functions (`simulate`, `translate`, + `flatten`, library loading, result export). + +The pipeline is FRONTEND (parse → SCode → instantiate → type → flatten) → +BACKEND (BDAE → SimCode → code generation) → SIMULATE. diff --git a/docs/src/installation.md b/docs/src/installation.md new file mode 100644 index 0000000..afc59c1 --- /dev/null +++ b/docs/src/installation.md @@ -0,0 +1,61 @@ +# Installation + +OM.jl is a multi-package project: the compiler stages live in sibling +sub-packages (`OMFrontend.jl`, `OMBackend.jl`, `OMParser.jl`, …) that the +top-level `OM` package references through `Project.toml` `[sources]` paths. +Those sub-packages are git **submodules**, so they must be checked out. + +## Requirements + +- **Julia ≥ 1.12** (the project's `[compat]` pins `julia = "1.12"`, and the + `[sources]` table requires Julia ≥ 1.11). +- Git. + +## Clone with submodules + +```bash +git clone --recurse-submodules https://github.com/JKRT/OM.jl.git +cd OM.jl +``` + +If you already cloned without `--recurse-submodules`: + +```bash +git submodule update --init --recursive +``` + +## Instantiate + +OM.jl depends on packages registered in the General registry **and** in the +OpenModelica registry, so add both before instantiating: + +```julia +julia --project -e ' + import Pkg + Pkg.Registry.add("General") + Pkg.Registry.add(Pkg.RegistrySpec(url="https://github.com/OpenModelica/OpenModelicaRegistry.git")) + Pkg.instantiate() + Pkg.build(; verbose = true)' +``` + +## First run + +```julia +using OM +sol = OM.simulate("Modelica.Mechanics.MultiBody.Examples.Elementary.Pendulum"; + MSL_Version = "MSL:3.2.3", stopTime = 1.0) +``` + +The first call compiles a large dependency tree (ModelingToolkit, +DifferentialEquations); subsequent runs in the same session are fast. + +## Other MSL versions and third-party libraries + +`MSL_Version` selects the bundled serialized MSL (3.2.3 or 4.0.0). To use an +installed-but-unbundled version or a third-party library, load it through the +general loader and pass the returned key: + +```julia +key = OM.loadInstalledLibrary("Modelica"; version = "4.1.0") +OM.simulate("Modelica....SomeModel"; libraries = [key]) +```