From aff13aa9853f4e934296c309025b206f27e0d51e Mon Sep 17 00:00:00 2001 From: Bart Date: Mon, 13 Jul 2026 12:30:47 +0200 Subject: [PATCH 1/6] Remove ControlPlots; migrate plotting to Makie + MakieControlPlots Redone fresh on current main (the original PR #248 branch predated main's NeuralFoil reorganization into the AirfoilAero/ObjAdapter submodules and could not be merged cleanly). - Remove the ControlPlots extension, examples_cp/, and the PythonCall/Matplotlib setup (bin/install_controlplots, CondaPkg LocalPreferences defaults). - Collapse the PlotBackend/MakieBackend/ControlPlotsBackend/set_plot_backend\! dispatch; plotting works once a Makie backend and MakieControlPlots are loaded. Drop the never-implemented plot_circulation_distribution. - VortexStepMethodMakieExt now triggers on ["Makie", "MakieControlPlots"] and renders plot_section_polars through MakieControlPlots.plot; everything else stays native Makie. - Examples/docs/tests load GLMakie/CairoMakie + MakieControlPlots. - Regenerate the 1.11 and 1.12 default manifests; drop dead :PyCall Aqua ignores. Validation (headless, Julia 1.12): plotting 58/58, Aqua 10/10; package loads on 1.11 and 1.12 with the regenerated manifests. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 8 + Manifest-v1.11.toml.default | 26 +- Manifest-v1.12.toml.default | 132 ++- Project.toml | 11 +- README.md | 18 +- bin/install | 42 +- bin/install_controlplots | 348 -------- bin/jetls_examples | 3 - bin/run_julia | 2 +- docs/Project.toml | 1 + docs/make.jl | 1 + docs/src/examples.md | 46 +- docs/src/functions.md | 2 +- docs/src/index.md | 6 - docs/src/private_functions.md | 3 +- examples/Project.toml | 1 + examples/V3_neuralfoil.jl | 1 + examples/linearize_check.jl | 1 + examples/menu.jl | 3 +- examples/obj_to_yaml_kite.jl | 1 + examples/ram_air_kite.jl | 1 + examples/show_slices.jl | 1 + examples_cp/LocalPreferences.toml.default | 5 - examples_cp/Project.toml | 11 - examples_cp/menu_cp.jl | 75 -- ext/VortexStepMethodControlPlotsExt.jl | 943 ---------------------- ext/VortexStepMethodMakieExt.jl | 87 +- src/VortexStepMethod.jl | 96 +-- src/obj_adapter/ObjAdapter.jl | 7 +- test/Aqua.jl | 3 +- test/LocalPreferences.toml.default | 2 - test/Project.toml | 6 +- test/plotting/test_backend_coexistence.jl | 38 - test/plotting/test_plotting.jl | 185 ++--- test/runtests.jl | 58 +- test/test_setup.sh | 2 +- 36 files changed, 272 insertions(+), 1904 deletions(-) delete mode 100755 bin/install_controlplots delete mode 100644 examples_cp/LocalPreferences.toml.default delete mode 100644 examples_cp/Project.toml delete mode 100644 examples_cp/menu_cp.jl delete mode 100644 ext/VortexStepMethodControlPlotsExt.jl delete mode 100644 test/LocalPreferences.toml.default delete mode 100644 test/plotting/test_backend_coexistence.jl diff --git a/CHANGELOG.md b/CHANGELOG.md index 3df78274..41d7bda0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,9 @@ instead of the old `ObjWing` pipeline (XFoil + single `.dat` file); `ObjWing` is kept as a shim that accepts but ignores `dat_path` - `Section` and `add_section!` accept an optional `cp_data` argument +- plotting is now Makie-only; the `VortexStepMethodMakieExt` extension loads once a + Makie backend and [`MakieControlPlots`](https://github.com/OpenSourceAWE/MakieControlPlots.jl) + are available, and `plot_section_polars` is rendered through `MakieControlPlots` ### Removed - `ObjWing` as a standalone pipeline (replaced by `ObjAdapter`); the name is @@ -43,6 +46,11 @@ - `PanelGroupingMethod` enum (already removed in v3.0.0 — stale docs entry cleaned up) - ControlPlots test run removed from CI (`plot-controlplots` arg) due to a `libraqm`/HarfBuzz symbol conflict in the GitHub Actions environment +- the `ControlPlots` plotting extension, `examples_cp/`, and the `PythonCall`/Matplotlib + setup (`bin/install_controlplots`, CondaPkg `LocalPreferences` defaults) +- the `PlotBackend`/`MakieBackend`/`ControlPlotsBackend` types and `set_plot_backend!`; + plotting works as soon as a Makie backend and `MakieControlPlots` are loaded +- the never-implemented `plot_circulation_distribution` ## VortexStepMethod v3.3.6 2026-06-13 diff --git a/Manifest-v1.11.toml.default b/Manifest-v1.11.toml.default index 43306bba..9db98457 100644 --- a/Manifest-v1.11.toml.default +++ b/Manifest-v1.11.toml.default @@ -2,7 +2,7 @@ julia_version = "1.11.9" manifest_format = "2.0" -project_hash = "3afeaa867282214c3057ab37347a1bbb8af6fe92" +project_hash = "6d819b6b32f1312b2d31d1cba26887098fe3abd1" [[deps.ADTypes]] git-tree-sha1 = "f7304359109c768cf32dc5fa2d371565bb63b68a" @@ -317,6 +317,18 @@ git-tree-sha1 = "acebe244d53ee1b461970f8910c235b259e772ef" uuid = "9aa1b823-49e4-5ca5-8b0f-3971ec8bab6a" version = "0.3.2" +[[deps.FileIO]] +deps = ["Pkg", "Requires", "UUIDs"] +git-tree-sha1 = "8e9c059d6857607253e837730dbf780b6b151acd" +uuid = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +version = "1.19.0" + + [deps.FileIO.extensions] + HTTPExt = "HTTP" + + [deps.FileIO.weakdeps] + HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" + [[deps.FileWatching]] uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" version = "1.11.0" @@ -649,6 +661,12 @@ version = "1.11.0" uuid = "14a3606d-f60d-562e-9121-12d972cd8159" version = "2023.12.12" +[[deps.NPZ]] +deps = ["FileIO", "ZipFile"] +git-tree-sha1 = "60a8e272fe0c5079363b28b0953831e2dd7b7e6f" +uuid = "15e1cf62-19b3-5cfa-8e77-841668bca605" +version = "0.4.3" + [[deps.NaNMath]] deps = ["OpenLibm_jll"] git-tree-sha1 = "9b8215b1ee9e78a293f99797cd31375471b2bcae" @@ -1165,6 +1183,12 @@ git-tree-sha1 = "a1c0c7585346251353cddede21f180b96388c403" uuid = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" version = "0.4.16" +[[deps.ZipFile]] +deps = ["Libdl", "Printf", "Zlib_jll"] +git-tree-sha1 = "f492b7fe1698e623024e873244f10d89c95c340a" +uuid = "a5390f91-8eb1-5f08-bee0-b1d1ffed6cea" +version = "0.10.1" + [[deps.Zlib_jll]] deps = ["Libdl"] uuid = "83775a58-1f1d-513f-b197-d71354ab007a" diff --git a/Manifest-v1.12.toml.default b/Manifest-v1.12.toml.default index 3d686d1c..5c390323 100644 --- a/Manifest-v1.12.toml.default +++ b/Manifest-v1.12.toml.default @@ -1,8 +1,8 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.12.6" +julia_version = "1.12.5" manifest_format = "2.0" -project_hash = "356626525ca9bb0f9ff17fd7cd0e99e15eac7f01" +project_hash = "30adb65bf2e48d1c0cd0a19da51c375ecdec07ec" [[deps.ADTypes]] git-tree-sha1 = "bbc22a9a08a0ef6460041086d8a7b27940ed4ffd" @@ -248,6 +248,23 @@ weakdeps = ["SparseArrays"] [deps.ChainRulesCore.extensions] ChainRulesCoreSparseArraysExt = "SparseArrays" +[[deps.ChunkCodecCore]] +git-tree-sha1 = "1a3ad7e16a321667698a19e77362b35a1e94c544" +uuid = "0b6fb165-00bc-4d37-ab8b-79f91016dbe1" +version = "1.0.1" + +[[deps.ChunkCodecLibZlib]] +deps = ["ChunkCodecCore", "Zlib_jll"] +git-tree-sha1 = "d4101e848e8d3f585d61d244c2fe0c80a70e6b3b" +uuid = "4c0bbee4-addc-4d73-81a0-b6caacae83c8" +version = "1.1.0" + +[[deps.ChunkCodecLibZstd]] +deps = ["ChunkCodecCore", "Zstd_jll"] +git-tree-sha1 = "34d9873079e4cb3d0c62926a225136824677073f" +uuid = "55437552-ac27-4d47-9aa3-63184e8fd398" +version = "1.0.0" + [[deps.CodecZlib]] deps = ["TranscodingStreams", "Zlib_jll"] git-tree-sha1 = "962834c22b66e32aa10f7611c08c8ca4e20749a9" @@ -354,12 +371,6 @@ git-tree-sha1 = "21d088c496ea22914fe80906eb5bce65755e5ec8" uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb" version = "2.5.1" -[[deps.CondaPkg]] -deps = ["JSON", "Markdown", "MicroMamba", "Pidfile", "Pkg", "Preferences", "Scratch", "TOML", "pixi_jll"] -git-tree-sha1 = "2b1afb8ae65a0758795b00adafb37f97e67ef0e9" -uuid = "992eb4ea-22a4-4c89-a5bb-47a3300528ab" -version = "0.2.36" - [[deps.ConstructionBase]] git-tree-sha1 = "b4b092499347b18a015186eae3042f72267106cb" uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" @@ -376,18 +387,6 @@ git-tree-sha1 = "439e35b0b36e2e5881738abc8857bd92ad6ff9a8" uuid = "d38c429a-6771-53c6-b99e-75d170b6e991" version = "0.6.3" -[[deps.ControlPlots]] -deps = ["JLD2", "LaTeXStrings", "Pkg", "Printf", "PythonPlot", "StaticArraysCore"] -git-tree-sha1 = "6cbab46fef00616339eb4c3e10faeb11b408a9cc" -uuid = "23c2ee80-7a9e-4350-b264-8e670f12517c" -version = "0.3.0" - - [deps.ControlPlots.extensions] - ControlPlotsExt = "ControlSystemsBase" - - [deps.ControlPlots.weakdeps] - ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e" - [[deps.CoreMath]] deps = ["CoreMath_jll"] git-tree-sha1 = "8c0480f92b1b1796239156a1b9b1bfb1b39499b4" @@ -1062,10 +1061,10 @@ uuid = "82899510-4779-5014-852e-03e436cf321d" version = "1.0.0" [[deps.JLD2]] -deps = ["FileIO", "MacroTools", "Mmap", "OrderedCollections", "PrecompileTools", "ScopedValues", "TranscodingStreams"] -git-tree-sha1 = "d97791feefda45729613fafeccc4fbef3f539151" +deps = ["ChunkCodecLibZlib", "ChunkCodecLibZstd", "FileIO", "MacroTools", "Mmap", "OrderedCollections", "PrecompileTools", "ScopedValues"] +git-tree-sha1 = "9ebadf3f8f0de07031359917549bbdadc23f5dc3" uuid = "033835bb-8acc-5ee8-8aae-3f567f8a3819" -version = "0.5.15" +version = "0.6.5" weakdeps = ["UnPack"] [deps.JLD2.extensions] @@ -1370,6 +1369,18 @@ version = "0.24.9" [deps.Makie.weakdeps] DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821" +[[deps.MakieControlPlots]] +deps = ["CairoMakie", "GLMakie", "GridLayoutBase", "JLD2", "LaTeXStrings", "Makie", "Pkg", "PrecompileTools", "Printf", "StaticArraysCore"] +git-tree-sha1 = "3f5e12bb0d1cd071758cd0c2a9058ddf3d2a6a31" +uuid = "6d616b69-6563-4f6e-8472-6f6c706c6f74" +version = "0.1.7" + + [deps.MakieControlPlots.extensions] + MakieControlPlotsControlSystemsBaseExt = "ControlSystemsBase" + + [deps.MakieControlPlots.weakdeps] + ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e" + [[deps.MappedArrays]] git-tree-sha1 = "0ee4497a4e80dbd29c058fcee6493f5219556f40" uuid = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900" @@ -1425,12 +1436,6 @@ git-tree-sha1 = "c009236e222df68e554c7ce5c720e4a33cc0c23f" uuid = "7269a6da-0436-5bbc-96c2-40638cbb6118" version = "0.5.3" -[[deps.MicroMamba]] -deps = ["Pkg", "Scratch", "micromamba_jll"] -git-tree-sha1 = "535656ce55266bfed0575cd051acc4f36dc869a0" -uuid = "0b3b1443-0f03-428d-bdfb-f27f9c1191ea" -version = "0.1.15" - [[deps.Missings]] deps = ["DataAPI"] git-tree-sha1 = "ec4f7fbeab05d7747bdf98eb74d130a2a2ed298d" @@ -1462,6 +1467,12 @@ git-tree-sha1 = "cac9cc5499c25554cba55cd3c30543cff5ca4fab" uuid = "46d2c3a1-f734-5fdb-9937-b9b9aeba4221" version = "0.2.4" +[[deps.NPZ]] +deps = ["FileIO", "ZipFile"] +git-tree-sha1 = "60a8e272fe0c5079363b28b0953831e2dd7b7e6f" +uuid = "15e1cf62-19b3-5cfa-8e77-841668bca605" +version = "0.4.3" + [[deps.NaNMath]] deps = ["OpenLibm_jll"] git-tree-sha1 = "9b8215b1ee9e78a293f99797cd31375471b2bcae" @@ -1702,12 +1713,6 @@ git-tree-sha1 = "5d5e0a78e971354b1c7bff0655d11fdc1b0e12c8" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" version = "2.8.4" -[[deps.Pidfile]] -deps = ["FileWatching", "Test"] -git-tree-sha1 = "2d8aaf8ee10df53d0dfb9b8ee44ae7c04ced2b03" -uuid = "fa939f87-e72e-5be4-a000-7fc836dbe307" -version = "1.3.0" - [[deps.Pixman_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LLVMOpenMP_jll", "Libdl"] git-tree-sha1 = "e4a6721aa89e62e5d4217c0b21bd714263779dda" @@ -1813,26 +1818,6 @@ git-tree-sha1 = "4fbbafbc6251b883f4d2705356f3641f3652a7fe" uuid = "43287f4e-b6f4-7ad1-bb20-aadabca52c3d" version = "1.4.0" -[[deps.PythonCall]] -deps = ["CondaPkg", "Dates", "Libdl", "MacroTools", "Markdown", "Preferences", "Serialization", "Tables", "UnsafePointers"] -git-tree-sha1 = "84e5ad9f90856963f4b17cfe12872598e731082c" -uuid = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" -version = "0.9.34" - - [deps.PythonCall.extensions] - CategoricalArraysExt = "CategoricalArrays" - PyCallExt = "PyCall" - - [deps.PythonCall.weakdeps] - CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" - PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" - -[[deps.PythonPlot]] -deps = ["Colors", "CondaPkg", "LaTeXStrings", "PythonCall", "Sockets", "Test", "VersionParsing"] -git-tree-sha1 = "409884283434a04092ddf1d9594c22bc097d5d9a" -uuid = "274fc56d-3b97-40fa-a1cd-1b4a50311bf9" -version = "1.0.6" - [[deps.QOI]] deps = ["ColorTypes", "FileIO", "FixedPointNumbers"] git-tree-sha1 = "472daaa816895cb7aee81658d4e7aec901fa1106" @@ -2436,26 +2421,15 @@ version = "1.28.0" NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" -[[deps.UnsafePointers]] -git-tree-sha1 = "c81331b3b2e60a982be57c046ec91f599ede674a" -uuid = "e17b2a0c-0bdf-430a-bd0c-3a23cae4ff39" -version = "1.0.0" - -[[deps.VersionParsing]] -git-tree-sha1 = "58d6e80b4ee071f5efd07fda82cb9fbe17200868" -uuid = "81def892-9a0e-5fdd-b105-ffc91e053289" -version = "1.3.0" - [[deps.VortexStepMethod]] -deps = ["Colors", "DefaultApplication", "DelimitedFiles", "DifferentiationInterface", "FiniteDiff", "ForwardDiff", "Interpolations", "LaTeXStrings", "LinearAlgebra", "Logging", "Measures", "NonlinearSolve", "Parameters", "Pkg", "PreallocationTools", "PrecompileTools", "RecursiveArrayTools", "SciMLBase", "Serialization", "StaticArrays", "Statistics", "StructMapping", "Timers", "Xfoil", "YAML"] +deps = ["Colors", "DefaultApplication", "DelimitedFiles", "DifferentiationInterface", "FiniteDiff", "ForwardDiff", "Interpolations", "LaTeXStrings", "LinearAlgebra", "Logging", "Measures", "NPZ", "NonlinearSolve", "Parameters", "Pkg", "PreallocationTools", "PrecompileTools", "Printf", "RecursiveArrayTools", "SciMLBase", "Serialization", "StaticArrays", "Statistics", "StructMapping", "Timers", "Xfoil", "YAML"] path = "." uuid = "ed3cd733-9f0f-46a9-93e0-89b8d4998dd9" -version = "3.3.4" -weakdeps = ["ControlPlots", "Makie", "PythonCall"] +version = "3.3.6" +weakdeps = ["Makie", "MakieControlPlots"] [deps.VortexStepMethod.extensions] - VortexStepMethodControlPlotsExt = ["ControlPlots", "PythonCall"] - VortexStepMethodMakieExt = "Makie" + VortexStepMethodMakieExt = ["Makie", "MakieControlPlots"] [[deps.Wayland_jll]] deps = ["Artifacts", "EpollShim_jll", "Expat_jll", "JLLWrappers", "Libdl", "Libffi_jll"] @@ -2600,6 +2574,12 @@ git-tree-sha1 = "a1c0c7585346251353cddede21f180b96388c403" uuid = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" version = "0.4.16" +[[deps.ZipFile]] +deps = ["Libdl", "Printf", "Zlib_jll"] +git-tree-sha1 = "f492b7fe1698e623024e873244f10d89c95c340a" +uuid = "a5390f91-8eb1-5f08-bee0-b1d1ffed6cea" +version = "0.10.1" + [[deps.Zlib_jll]] deps = ["Libdl"] uuid = "83775a58-1f1d-513f-b197-d71354ab007a" @@ -2682,12 +2662,6 @@ git-tree-sha1 = "4e4282c4d846e11dce56d74fa8040130b7a95cb3" uuid = "c5f90fcd-3b7e-5836-afba-fc50a0988cb2" version = "1.6.0+0" -[[deps.micromamba_jll]] -deps = ["Artifacts", "JLLWrappers", "LazyArtifacts", "Libdl"] -git-tree-sha1 = "717df6f6892af4ee13279a73aa58474e58a88667" -uuid = "f8abcde7-e9b7-5caa-b8af-a437887ae8e4" -version = "2.3.1+0" - [[deps.nghttp2_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" @@ -2704,12 +2678,6 @@ deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" version = "17.7.0+0" -[[deps.pixi_jll]] -deps = ["Artifacts", "JLLWrappers", "LazyArtifacts", "Libdl"] -git-tree-sha1 = "3667b0931a7fe50f0a5554c61af00e5640019e21" -uuid = "4d7b5844-a134-5dcd-ac86-c8f19cd51bed" -version = "0.63.2+0" - [[deps.x264_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] git-tree-sha1 = "14cc7083fc6dff3cc44f2bc435ee96d06ed79aa7" diff --git a/Project.toml b/Project.toml index 091c8669..43ad0854 100644 --- a/Project.toml +++ b/Project.toml @@ -4,7 +4,7 @@ authors = ["1-Bart-1 ", "Oriol Cayon and contributors"] version = "3.3.6" [workspace] -projects = ["examples", "examples_cp", "docs", "test"] +projects = ["examples", "docs", "test"] [deps] Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" @@ -36,17 +36,14 @@ Xfoil = "19641d66-a62d-11e8-2441-8f57a969a9c4" YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" [weakdeps] -ControlPlots = "23c2ee80-7a9e-4350-b264-8e670f12517c" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" -PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" +MakieControlPlots = "6d616b69-6563-4f6e-8472-6f6c706c6f74" [extensions] -VortexStepMethodControlPlotsExt = ["ControlPlots", "PythonCall"] -VortexStepMethodMakieExt = "Makie" +VortexStepMethodMakieExt = ["Makie", "MakieControlPlots"] [compat] Colors = "0.13" -ControlPlots = "0.2.5, 0.3" DefaultApplication = "1" DelimitedFiles = "1" DifferentiationInterface = "0.7.4" @@ -57,6 +54,7 @@ LaTeXStrings = "1" LinearAlgebra = "1" Logging = "1" Makie = "0.24.6" +MakieControlPlots = "0.1.5" Measures = "0.3" NPZ = "0.4" NonlinearSolve = "4.8.0" @@ -64,7 +62,6 @@ Parameters = "0.12" Pkg = "1" Printf = "1" PreallocationTools = "1.1.2" -PythonCall = "0.9" PrecompileTools = "1.2.1" RecursiveArrayTools = "3, 4" SciMLBase = "2, 3" diff --git a/README.md b/README.md index b05c0972..d43294c9 100644 --- a/README.md +++ b/README.md @@ -20,14 +20,7 @@ Example output is shown in the figure below, where the aerodynamic predictions o ## Installation Install [Julia 1.10](https://ufechner7.github.io/2024/08/09/installing-julia-with-juliaup.html) or later, -if you haven't already. On Linux, make sure that Python3 and Matplotlib are installed: -``` -sudo apt install python3-matplotlib -``` -Furthermore, the package `ControlPlots` must be installed globally: -``` -julia -e 'using Pkg; Pkg.add("ControlPlots")' -``` +if you haven't already. Before installing this software it is suggested to create a new project, for example like this: @@ -62,10 +55,6 @@ julia> using VortexStepMethod julia> VortexStepMethod.install_examples() julia> include("examples/menu.jl") ``` -or, for using the ControlPlots library (faster time-to-first-plot): -``` -include("examples_cp/menu_cp.jl") -``` ## Running the examples as developer If you have git installed, check out this repo because it makes it easier to understand the code: @@ -88,11 +77,6 @@ Then you can display a menu with the available examples using the GLMakie librar ```julia menu() ``` -or using the ControlPlots library (faster time-to-first-plot): -```julia -menu_cp() -``` - To browse the code, it is suggested to use [VSCode](https://code.visualstudio.com/) with the Julia plugin. diff --git a/bin/install b/bin/install index cd48e024..c7f69397 100755 --- a/bin/install +++ b/bin/install @@ -187,7 +187,7 @@ if ! instantiate_main_with_fallback "." "main"; then exit 1 fi -SUBPROJECTS=("examples" "examples_cp" "test" "docs" "scripts") +SUBPROJECTS=("examples" "test" "docs" "scripts") echo echo "Instantiating sub-projects..." @@ -226,46 +226,10 @@ else exit 1 fi -# Bootstrap LocalPreferences.toml for subprojects that need PythonCall. -# Only copies the .default if no LocalPreferences.toml exists yet (e.g. fresh -# clone). A subsequent run of bin/install_controlplots will add or update the -# machine-specific settings on top. -echo -echo "Bootstrapping LocalPreferences.toml for subprojects..." -for _lp_dir in examples_cp test; do - _lp_file="${_lp_dir}/LocalPreferences.toml" - _lp_default="${_lp_dir}/LocalPreferences.toml.default" - if [[ ! -f "$_lp_file" && -f "$_lp_default" ]]; then - cp "$_lp_default" "$_lp_file" - echo " Created ${_lp_file} from default (CondaPkg disabled)." - fi -done - $_julia_cmd --project=. -e 'using Pkg; Pkg.precompile(); Pkg.activate("examples"); Pkg.precompile(); -Pkg.activate("examples_cp"); Pkg.precompile(); Pkg.activate("test"); Pkg.precompile(); +Pkg.activate("test"); Pkg.precompile(); @info "Precompilation complete."' -$_julia_cmd --project=examples -e 'using GLMakie, VortexStepMethod; @info "GLMakie extension ready."' -_cp_lp="examples_cp/LocalPreferences.toml" -# Skip ControlPlots verification only when Python is genuinely not yet configured -# (Null backend with no exe= set). After install_controlplots --system the file -# still has backend="Null" but also has exe=..., so we proceed with verification. -if grep -q 'backend = "Null"' "$_cp_lp" 2>/dev/null && ! grep -q 'exe = ' "$_cp_lp" 2>/dev/null; then - echo "Skipping ControlPlots extension verification (Python not yet configured)." - echo "Run ./bin/install_controlplots to set up Python and verify ControlPlots." -else - # On Linux, prepend Julia's lib/julia so its bundled libcrypto.so.3 takes - # priority over an older system OpenSSL (e.g. Ubuntu 22.04 ships 3.0 while - # Julia 1.12 bundles 3.3+). - _julia_lib=$($_julia_cmd -e 'print(joinpath(Sys.BINDIR, "..", "lib", "julia"))' 2>/dev/null || true) - if [[ "$(uname -s)" == "Linux" && -n "$_julia_lib" && -d "$_julia_lib" ]]; then - LD_LIBRARY_PATH="${_julia_lib}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" \ - $_julia_cmd -t1 --project=examples_cp \ - -e 'using ControlPlots, VortexStepMethod; @info "ControlPlots extension ready."' - else - $_julia_cmd -t1 --project=examples_cp \ - -e 'using ControlPlots, VortexStepMethod; @info "ControlPlots extension ready."' - fi -fi +$_julia_cmd --project=examples -e 'using GLMakie, MakieControlPlots, VortexStepMethod; @info "Makie plotting extension ready."' $_julia_cmd --project=. -e 'using Pkg; Pkg.test(test_args=["settings/test_settings.jl"]); @info "Minimal test smoke check complete."' diff --git a/bin/install_controlplots b/bin/install_controlplots deleted file mode 100755 index 59a8abfd..00000000 --- a/bin/install_controlplots +++ /dev/null @@ -1,348 +0,0 @@ -#!/bin/bash -eu -# SPDX-FileCopyrightText: 2025 Uwe Fechner -# SPDX-License-Identifier: MIT -# -# Install and configure matplotlib for ControlPlots.jl (via PythonCall). -# -# Two backends are supported: -# 1) System Python – uses the matplotlib package installed by Ubuntu/Debian (apt). -# Fastest option; shares the system Python install. -# 2) CondaPkg – installs matplotlib into a pixi-managed Conda environment. -# Self-contained; does not require root / sudo. -# -# Both options configure the Qt (qtagg) backend for interactive plot windows. - -print_usage() { - echo "Usage:" - echo " ./bin/install_controlplots [--system | --conda] [-y | --yes] [-h | --help]" - echo "" - echo "Options:" - echo " --system Use the system Python and matplotlib (Ubuntu/Debian apt)" - echo " --conda Use CondaPkg (pixi) to install matplotlib" - echo " -y, --yes Non-interactive; accept defaults" - echo " -h, --help Show this help message" -} - -_backend="" # "system" or "conda" -_yes=false - -while [[ $# -gt 0 ]]; do - case $1 in - -h|--help) - print_usage - exit 0 - ;; - --system) - _backend="system" - shift - ;; - --conda) - _backend="conda" - shift - ;; - -y|--yes) - _yes=true - shift - ;; - *) - echo "Unknown option: $1" - print_usage - exit 1 - ;; - esac -done - -# Always run from the repository root (resolve from this script location). -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -cd "${REPO_ROOT}" - -CONTROLPLOTS_PROJECT="examples_cp" -TEST_PROJECT="test" - -# ── Interactive backend selection ───────────────────────────────────────────── - -if [[ -z "$_backend" ]]; then - echo "Which matplotlib backend do you want to use for ControlPlots?" - echo "" - echo " 1) System Python (uses Ubuntu/Debian python3-matplotlib via apt)" - echo " - Requires sudo" - echo " - Shares the system-wide Python installation" - echo "" - echo " 2) CondaPkg (installs matplotlib into a pixi-managed Conda environment)" - echo " - No sudo required" - echo " - Self-contained; downloads ~200 MB on first use" - echo "" - if [[ "$_yes" == true ]]; then - _choice="2" - echo "Using default: 2 (CondaPkg)" - else - read -rp "Enter 1 or 2 [default: 2]: " _choice - fi - case "${_choice:-2}" in - 1) _backend="system" ;; - 2) _backend="conda" ;; - *) - echo "Invalid choice. Please enter 1 or 2." - exit 1 - ;; - esac -fi - -echo "" -echo "Selected backend: $_backend" -echo "" - -# ── Helper: install matplotlib via CondaPkg ─────────────────────────────────── - -_install_matplotlib_condapkg() { - echo "Installing matplotlib and pyqt into CondaPkg (pixi) environment..." - julia --project="${CONTROLPLOTS_PROJECT}" -e ' -using Pkg -# Ensure CondaPkg is available -if Base.find_package("CondaPkg") === nothing - Pkg.add("CondaPkg") -end -using CondaPkg -CondaPkg.add("matplotlib") -CondaPkg.add("pyqt") -CondaPkg.resolve() -println("matplotlib and pyqt installed in CondaPkg environment.") -' -} - -_verify_controlplots() { - echo "Verifying ControlPlots can be loaded..." - local _julia_prefix="" - if [[ "$(uname -s)" == "Linux" ]]; then - # Julia 1.12+ ships libssl.so.3 compiled against OpenSSL 3.3+, which may - # not match the system libcrypto.so.3 (e.g. Ubuntu 22.04 ships 3.0). - # Prepend Julia's own lib/julia directory so its bundled libcrypto.so.3 - # is found before the system one. We keep any existing LD_LIBRARY_PATH - # entries so other libraries set by callers are not lost. - _julia_lib=$(julia -e 'print(joinpath(Sys.BINDIR, "..", "lib", "julia"))' 2>/dev/null || true) - if [[ -n "$_julia_lib" && -d "$_julia_lib" ]]; then - _julia_prefix="env LD_LIBRARY_PATH=${_julia_lib}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" - fi - fi - if $_julia_prefix julia --project="${CONTROLPLOTS_PROJECT}" -e ' -using ControlPlots -println("ControlPlots loaded successfully.") -'; then - echo "" - echo "✓ ControlPlots is working." - else - echo "" - echo "Warning: ControlPlots could not be loaded. Check the error output above." - echo "You may need to run this script again with the other backend option." - exit 1 - fi -} - -# ── Helper: write MPLBACKEND=qtagg to LocalPreferences.toml ────────────────── - -_set_mplbackend_qtagg() { - local _prefs_file="${CONTROLPLOTS_PROJECT}/LocalPreferences.toml" - # Remove any existing [ENV] section managed by this script. - if [[ -f "$_prefs_file" ]]; then - _tmp=$(mktemp) - awk ' - /^\[ENV\]/ { in_env=1; next } - in_env && /^\[/ { in_env=0 } - !in_env { print } - ' "$_prefs_file" > "$_tmp" - mv "$_tmp" "$_prefs_file" - fi - { - echo "" - echo "[ENV]" - echo "MPLBACKEND = \"qtagg\"" - } >> "$_prefs_file" - echo "Set MPLBACKEND=qtagg in $_prefs_file." -} - -# ── System Python backend ───────────────────────────────────────────────────── - -if [[ "$_backend" == "system" ]]; then - # Detect package manager and install python3-matplotlib if missing. - if [[ "$(uname -s)" == "Linux" ]]; then - if grep -qiE "ubuntu|debian" /etc/os-release 2>/dev/null; then - if ! dpkg -s python3-matplotlib &>/dev/null 2>&1 || ! dpkg -s python3-pyqt5 &>/dev/null 2>&1; then - echo "Installing python3-matplotlib and python3-pyqt5 via apt..." - sudo apt install -y python3-matplotlib python3-pyqt5 - else - echo "python3-matplotlib and python3-pyqt5 are already installed." - fi - elif grep -qi "fedora" /etc/os-release 2>/dev/null; then - if ! rpm -q python3-matplotlib &>/dev/null 2>&1 || ! rpm -q python3-pyqt5 &>/dev/null 2>&1; then - echo "Installing python3-matplotlib and python3-qt5 via dnf..." - sudo dnf install -y python3-matplotlib python3-pyqt5 - else - echo "python3-matplotlib and python3-pyqt5 are already installed." - fi - else - echo "Warning: Could not detect Ubuntu/Debian or Fedora." - echo "Please ensure python3-matplotlib is installed manually before continuing." - if [[ "$_yes" == false ]]; then - read -rp "Continue anyway? (y/n) [default: y]: " _cont - case "${_cont:-y}" in - n|N) echo "Aborted."; exit 1 ;; - esac - fi - fi - elif [[ "$(uname -s)" =~ ^(MINGW|MSYS|CYGWIN) ]]; then - # Windows (Git Bash): use pip to install matplotlib and PyQt5. - _pip="" - for _pip_candidate in pip3 pip; do - if command -v "$_pip_candidate" &>/dev/null; then - _pip="$_pip_candidate" - break - fi - done - if [[ -n "$_pip" ]]; then - if ! "$_pip" show matplotlib &>/dev/null 2>&1 || ! "$_pip" show PyQt5 &>/dev/null 2>&1; then - echo "Installing matplotlib and PyQt5 via pip..." - "$_pip" install --user matplotlib PyQt5 - else - echo "matplotlib and PyQt5 are already installed." - fi - else - echo "Warning: pip not found. Please install matplotlib and PyQt5 manually:" - echo " pip install matplotlib PyQt5" - if [[ "$_yes" == false ]]; then - read -rp "Continue anyway? (y/n) [default: y]: " _cont - case "${_cont:-y}" in - n|N) echo "Aborted."; exit 1 ;; - esac - fi - fi - else - echo "Warning: System Python backend is intended for Ubuntu/Debian/Fedora Linux or Windows." - echo "On this OS ($(uname -s)) you may need to install matplotlib manually." - if [[ "$_yes" == false ]]; then - read -rp "Continue anyway? (y/n) [default: y]: " _cont - case "${_cont:-y}" in - n|N) echo "Aborted."; exit 1 ;; - esac - fi - fi - - # Locate the system python3 executable. - _syspython="" - if [[ "$(uname -s)" =~ ^(MINGW|MSYS|CYGWIN) ]]; then - # On Windows (Git Bash) 'python3' may not exist; also try 'python'. - for _candidate in python3 python; do - if command -v "$_candidate" &>/dev/null; then - _syspython=$(command -v "$_candidate") - # Verify it runs (guards against the Windows Store stub). - if "$_syspython" --version &>/dev/null 2>&1; then - break - fi - _syspython="" - fi - done - else - for _candidate in python3 /usr/bin/python3; do - if command -v "$_candidate" &>/dev/null; then - _syspython=$(command -v "$_candidate") - break - fi - done - fi - if [[ -z "$_syspython" ]]; then - echo "Error: python3 not found on PATH. Please install python3." - exit 1 - fi - echo "Found system Python: $_syspython" - - # Remove the CondaPkg-managed environment so that PythonCall won't keep - # using a stale conda Python when switching to system Python. - if [[ -d "${CONTROLPLOTS_PROJECT}/.CondaPkg" ]]; then - echo "Removing ${CONTROLPLOTS_PROJECT}/.CondaPkg (switching from CondaPkg to system Python)..." - rm -rf "${CONTROLPLOTS_PROJECT}/.CondaPkg" - fi - if [[ -d "${TEST_PROJECT}/.CondaPkg" ]]; then - echo "Removing ${TEST_PROJECT}/.CondaPkg (switching from CondaPkg to system Python)..." - rm -rf "${TEST_PROJECT}/.CondaPkg" - fi - # Unset JULIA_PYTHONCALL_EXE so the LocalPreferences.toml exe setting takes - # effect. - unset JULIA_PYTHONCALL_EXE - # Prevent CondaPkg from re-installing a Conda env in the current process. - export JULIA_CONDAPKG_BACKEND="Null" - export JULIA_PYTHONCALL_EXE="$_syspython" - - # Helper: update a LocalPreferences.toml file with the system Python settings. - _write_system_python_prefs() { - local _pf="$1" - if [[ -f "$_pf" ]]; then - _tmp=$(mktemp) - awk ' - /^\[PythonCall\]/ { in_sec=1; next } - /^\[PyCall\]/ { in_sec=1; next } - /^\[CondaPkg\]/ { in_sec=1; next } - in_sec && /^\[/ { in_sec=0 } - !in_sec { print } - ' "$_pf" > "$_tmp" - mv "$_tmp" "$_pf" - fi - { - echo "" - echo "[PythonCall]" - echo "exe = \"$_syspython\"" - echo "" - echo "[CondaPkg]" - echo "backend = \"Null\"" - } >> "$_pf" - echo "Written to $_pf." - } - - echo "" - echo "Saving python_exe=$_syspython to LocalPreferences.toml files..." - # Write to the root project plus the subprojects that load PythonCall. - _write_system_python_prefs "LocalPreferences.toml" - _write_system_python_prefs "${CONTROLPLOTS_PROJECT}/LocalPreferences.toml" - _write_system_python_prefs "${TEST_PROJECT}/LocalPreferences.toml" - - _set_mplbackend_qtagg - _verify_controlplots - - echo "" - echo "Done. ControlPlots will use the system Python (PythonCall) matplotlib with the Qt (qtagg) backend." - -# ── CondaPkg backend ────────────────────────────────────────────────────────── - -elif [[ "$_backend" == "conda" ]]; then - # Remove [PythonCall] exe, [CondaPkg] backend override, and legacy [PyCall] - # so PythonCall falls back to the CondaPkg-managed Python. - # Must be done in both the root and examples_cp LocalPreferences.toml. - _remove_python_prefs() { - local _pf="$1" - [[ -f "$_pf" ]] || return 0 - _tmp=$(mktemp) - awk ' - /^\[PythonCall\]/ { in_sec=1; next } - /^\[PyCall\]/ { in_sec=1; next } - /^\[CondaPkg\]/ { in_sec=1; next } - in_sec && /^\[/ { in_sec=0 } - !in_sec { print } - ' "$_pf" > "$_tmp" - if ! diff -q "$_pf" "$_tmp" &>/dev/null; then - mv "$_tmp" "$_pf" - echo "Removed [PythonCall]/[CondaPkg] sections from $_pf (switching to CondaPkg)." - else - rm -f "$_tmp" - fi - } - _remove_python_prefs "LocalPreferences.toml" - _remove_python_prefs "${CONTROLPLOTS_PROJECT}/LocalPreferences.toml" - _remove_python_prefs "${TEST_PROJECT}/LocalPreferences.toml" - - _install_matplotlib_condapkg - - _set_mplbackend_qtagg - _verify_controlplots - - echo "" - echo "Done. ControlPlots will use the CondaPkg-managed matplotlib with the Qt (qtagg) backend." -fi diff --git a/bin/jetls_examples b/bin/jetls_examples index 2bcc466d..13a98e75 100755 --- a/bin/jetls_examples +++ b/bin/jetls_examples @@ -9,9 +9,6 @@ fi echo -e "\033[1mInstalling dependencies...\033[0m" julia --project=examples -e 'using Pkg; Pkg.instantiate()' -julia --project=examples_cp -e 'using Pkg; Pkg.instantiate()' echo -e "\033[1mChecking examples with jetls...\033[0m" jetls check --root=. examples/*.jl -echo -e "\033[1mChecking examples_cp with jetls...\033[0m" -jetls -t 1,0 -- check --root=. examples_cp/*.jl echo diff --git a/bin/run_julia b/bin/run_julia index 8408a0c7..3c14cce7 100755 --- a/bin/run_julia +++ b/bin/run_julia @@ -9,7 +9,7 @@ export JULIA_PKG_SERVER_REGISTRY_PREFERENCE=eager # If any local project preferences file disables CondaPkg, forward that as an # env var so that Julia reads it at runtime even when activating a subproject # interactively (where @load_preference may have a stale baked-in value). -for _lp in LocalPreferences.toml examples_cp/LocalPreferences.toml test/LocalPreferences.toml; do +for _lp in LocalPreferences.toml test/LocalPreferences.toml; do if grep -q 'backend = "Null"' "$_lp" 2>/dev/null; then export JULIA_CONDAPKG_BACKEND=Null break diff --git a/docs/Project.toml b/docs/Project.toml index 2039ea48..d7d84381 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -2,6 +2,7 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +MakieControlPlots = "6d616b69-6563-4f6e-8472-6f6c706c6f74" VortexStepMethod = "ed3cd733-9f0f-46a9-93e0-89b8d4998dd9" [sources] diff --git a/docs/make.jl b/docs/make.jl index 3b020e6e..b25fedbb 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,4 +1,5 @@ using Makie +using MakieControlPlots using VortexStepMethod using VortexStepMethod.AirfoilAero using VortexStepMethod.ObjAdapter diff --git a/docs/src/examples.md b/docs/src/examples.md index 36555701..5c7cf7ea 100644 --- a/docs/src/examples.md +++ b/docs/src/examples.md @@ -132,12 +132,6 @@ more examples by typing: julia> include("examples/menu.jl") ``` -or, you prefer to use the ControlPlots library: - -```julia -julia> include("examples_cp/menu_cp.jl") -``` - You should see the following menu: ```text @@ -161,7 +155,13 @@ Press `` to run the selected example. ## Plotting Backends -The examples in this package support three plotting backends. Here is a comparison to help you choose: +The examples use two Makie backends — `GLMakie` (interactive) and `CairoMakie` +(headless / vector output). For a +[ControlPlots](https://github.com/aenarete/ControlPlots.jl)-style plotting API on +top of Makie, the examples also load +[`MakieControlPlots`](https://github.com/OpenSourceAWE/MakieControlPlots.jl), +which provides `plot`, `plotx`, and `plotxy` helpers. Here is a comparison to +help you choose a backend: ### GLMakie **Advantages:** @@ -172,7 +172,6 @@ The examples in this package support three plotting backends. Here is a comparis **Disadvantages:** - Requires a display server (does not work in headless/server environments without a virtual framebuffer). - Heavier dependency: needs OpenGL drivers and a GPU. -- Longer initial load time compared to ControlPlots ### CairoMakie **Advantages:** @@ -183,29 +182,14 @@ The examples in this package support three plotting backends. Here is a comparis - Plots are static — no interactive zoom or pan. - Slower for very large or complex scenes because rendering is done in software. - 3D support is limited compared to GLMakie. -- Longer initial load time compared to ControlPlots -### ControlPlots (based on PyPlot / Matplotlib) -**Advantages:** -- Simple API, easy to learn for students -- In addition, the Matplotlib API for users coming from Python/Matplotlib is supported. -- Works in headless environments; can save to PNG, SVG, PDF, etc. -- Very lightweight Julia-side dependency (delegates work to Python). - -**Disadvantages:** -- Requires a working Python installation with Matplotlib (via `PyCall`). -- Might crash when multithreading is enabled. Start Julia with `-t 1,0` to avoid problems. -- No native Makie ecosystem integration (e.g. cannot use `Makie.Observable` for live updates). -- Interactivity is limited and depends on the Matplotlib backend in use. -- Extra setup complexity when Python or Matplotlib are not already installed. - -| Feature | GLMakie | CairoMakie | ControlPlots | -|---|---|---|---| -| Interactive (zoom/pan) | yes | no | yes | -| Headless / server | no* | yes | yes | -| Vector output (PDF/SVG) | no | yes | yes | -| GPU required | yes | no | no | -| 3D support | full | limited | limited | -| Load time | slow | medium | fast | +| Feature | GLMakie | CairoMakie | +|---|---|---| +| Interactive (zoom/pan) | yes | no | +| Headless / server | no* | yes | +| Vector output (PDF/SVG) | no | yes | +| GPU required | yes | no | +| 3D support | full | limited | +| Load time | slow | medium | \* GLMakie can run headless with a virtual framebuffer (e.g. `Xvfb`), but this requires additional setup. \ No newline at end of file diff --git a/docs/src/functions.md b/docs/src/functions.md index ebab0e69..74523e35 100644 --- a/docs/src/functions.md +++ b/docs/src/functions.md @@ -80,7 +80,7 @@ calculate_results ``` ## Main Plotting Functions -The plotting functions are implemented as [package extensions](https://pkgdocs.julialang.org/v1.11/creating-packages/#Conditional-loading-of-code-in-packages-(Extensions)). They are available when `GLMakie` (or `ControlPlots`) is loaded before `VortexStepMethod`. The examples use `GLMakie`. +The plotting functions are implemented as [package extensions](https://pkgdocs.julialang.org/v1.11/creating-packages/#Conditional-loading-of-code-in-packages-(Extensions)). They are available once a Makie backend (`GLMakie` or `CairoMakie`) and `MakieControlPlots` are loaded before `VortexStepMethod`. The examples use `GLMakie`. ```@docs plot_geometry plot_distribution diff --git a/docs/src/index.md b/docs/src/index.md index 8ef6e3d4..b5f0aa55 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -82,12 +82,6 @@ Then you can display a menu with the available examples using the GLMakie librar menu() ``` -or using the ControlPlots library (faster time-to-first-plot): - -```julia -menu_cp() -``` - To browse the code, it is suggested to use [VSCode](https://code.visualstudio.com/) with the Julia plugin. diff --git a/docs/src/private_functions.md b/docs/src/private_functions.md index 7cebb81d..2e96c6d9 100644 --- a/docs/src/private_functions.md +++ b/docs/src/private_functions.md @@ -82,10 +82,9 @@ prepare_cp_output! validate_cp_sections ``` -### Examples and plotting backend +### Examples ```@docs copy_examples -set_plot_backend! ``` ## Airfoil aerodynamics (AirfoilAero) diff --git a/examples/Project.toml b/examples/Project.toml index 051edc75..bb7e1f8d 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -8,6 +8,7 @@ DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +MakieControlPlots = "6d616b69-6563-4f6e-8472-6f6c706c6f74" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" VortexStepMethod = "ed3cd733-9f0f-46a9-93e0-89b8d4998dd9" YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" diff --git a/examples/V3_neuralfoil.jl b/examples/V3_neuralfoil.jl index 4976e3b4..1f3b2563 100644 --- a/examples/V3_neuralfoil.jl +++ b/examples/V3_neuralfoil.jl @@ -3,6 +3,7 @@ if Base.active_project() != joinpath(@__DIR__, "Project.toml") Pkg.activate(@__DIR__) end using GLMakie +using MakieControlPlots using VortexStepMethod using VortexStepMethod.ObjAdapter using VortexStepMethod.AirfoilAero: ShrinkWrap, NeuralFoilSolver, XFoilSolver diff --git a/examples/linearize_check.jl b/examples/linearize_check.jl index 629bed6b..63614520 100644 --- a/examples/linearize_check.jl +++ b/examples/linearize_check.jl @@ -4,6 +4,7 @@ if Base.active_project() != joinpath(@__DIR__, "Project.toml") end using GLMakie +using MakieControlPlots using DifferentiationInterface using LinearAlgebra using VortexStepMethod diff --git a/examples/menu.jl b/examples/menu.jl index 38a986c6..9714dbed 100644 --- a/examples/menu.jl +++ b/examples/menu.jl @@ -3,11 +3,10 @@ Pkg.activate(@__DIR__) using GLMakie using CairoMakie +using MakieControlPlots using VortexStepMethod using REPL.TerminalMenus -set_plot_backend!(MakieBackend()) - url = "https://opensourceawe.github.io/VortexStepMethod.jl/dev" example_files = [ diff --git a/examples/obj_to_yaml_kite.jl b/examples/obj_to_yaml_kite.jl index b01ededd..3a70342a 100644 --- a/examples/obj_to_yaml_kite.jl +++ b/examples/obj_to_yaml_kite.jl @@ -17,6 +17,7 @@ if Base.active_project() != joinpath(@__DIR__, "Project.toml") Pkg.activate(@__DIR__) end using GLMakie +using MakieControlPlots using VortexStepMethod using VortexStepMethod.ObjAdapter using VortexStepMethod.AirfoilAero: ShrinkWrap, NeuralFoilSolver diff --git a/examples/ram_air_kite.jl b/examples/ram_air_kite.jl index 695b3d09..c99f1eaf 100644 --- a/examples/ram_air_kite.jl +++ b/examples/ram_air_kite.jl @@ -3,6 +3,7 @@ if Base.active_project() != joinpath(@__DIR__, "Project.toml") Pkg.activate(@__DIR__) end using GLMakie +using MakieControlPlots using VortexStepMethod using VortexStepMethod.ObjAdapter using VortexStepMethod.AirfoilAero: XFoilSolver, NeuralFoilSolver, ShrinkWrap diff --git a/examples/show_slices.jl b/examples/show_slices.jl index c7552240..1199ba7e 100644 --- a/examples/show_slices.jl +++ b/examples/show_slices.jl @@ -3,6 +3,7 @@ if Base.active_project() != joinpath(@__DIR__, "Project.toml") Pkg.activate(@__DIR__) end using GLMakie +using MakieControlPlots using VortexStepMethod using VortexStepMethod.ObjAdapter diff --git a/examples_cp/LocalPreferences.toml.default b/examples_cp/LocalPreferences.toml.default deleted file mode 100644 index 99b6cf42..00000000 --- a/examples_cp/LocalPreferences.toml.default +++ /dev/null @@ -1,5 +0,0 @@ -[CondaPkg] -backend = "Null" - -[ENV] -MPLBACKEND = "qtagg" diff --git a/examples_cp/Project.toml b/examples_cp/Project.toml deleted file mode 100644 index c1d68765..00000000 --- a/examples_cp/Project.toml +++ /dev/null @@ -1,11 +0,0 @@ -[deps] -CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" -ControlPlots = "23c2ee80-7a9e-4350-b264-8e670f12517c" -DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" -DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" -LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" -VortexStepMethod = "ed3cd733-9f0f-46a9-93e0-89b8d4998dd9" - -[sources] -VortexStepMethod = {path = ".."} diff --git a/examples_cp/menu_cp.jl b/examples_cp/menu_cp.jl deleted file mode 100644 index 401d845a..00000000 --- a/examples_cp/menu_cp.jl +++ /dev/null @@ -1,75 +0,0 @@ -using Pkg -if !("ControlPlots" ∈ keys(Pkg.project().dependencies)) - Pkg.activate(@__DIR__) -end - -using ControlPlots -using VortexStepMethod -using REPL.TerminalMenus - -set_plot_backend!(ControlPlotsBackend()) - -url = "https://opensourceawe.github.io/VortexStepMethod.jl/dev" - -examples_dir = joinpath(@__DIR__, "..", "examples") - -example_files = [ - "V3_kite.jl", - "billowing.jl", - "pyramid_model.jl", - "rectangular_wing.jl", - "ram_air_kite.jl", - "stall_model.jl", - "bench.jl", - "cleanup.jl", -] - -function run_all() - failed_examples = String[] - for f in example_files - f == "cleanup.jl" && continue - println("\n" * "="^60) - println("Running: $f") - println("="^60) - try - include(joinpath(examples_dir, f)) - catch e - @error "Failed: $f" exception=(e, catch_backtrace()) - push!(failed_examples, f) - end - end - if isempty(failed_examples) - println("\nAll examples completed.") - else - failed_list = join(failed_examples, ", ") - throw(ErrorException("run_all failed for $(length(failed_examples)) example(s): $failed_list")) - end -end - -function example_menu() - options = [ - [("$(splitext(f)[1]) = include(\"../examples/$f\")") - for f in example_files]; - "help_me = VortexStepMethod.help(\"$url\")"; - "quit" - ] - active = true - while active - menu = RadioMenu(options, pagesize=8) - choice = request( - "\nChoose function to execute or `q` to quit: ", - menu) - if choice != -1 && choice != length(options) - eval(Meta.parse(options[choice])) - else - println("Left menu. Press to quit Julia!") - active = false - end - end -end - -if "--run-all" in ARGS - run_all() -else - example_menu() -end diff --git a/ext/VortexStepMethodControlPlotsExt.jl b/ext/VortexStepMethodControlPlotsExt.jl deleted file mode 100644 index dba0be96..00000000 --- a/ext/VortexStepMethodControlPlotsExt.jl +++ /dev/null @@ -1,943 +0,0 @@ -module VortexStepMethodControlPlotsExt -using ControlPlots, LaTeXStrings, VortexStepMethod, LinearAlgebra, Statistics, DelimitedFiles -using PythonCall: pyconvert -import ControlPlots: plt -import VortexStepMethod: calculate_filaments_for_plotting - -export plot_wing, plot_circulation_distribution, plot_geometry, plot_distribution, - plot_polars, save_plot, show_plot, plot_polar_data, plot_combined_analysis - -# Set this extension as the active plotting backend when loaded (only if not already set) -function __init__() - isnothing(VortexStepMethod._PLOT_BACKEND[]) && - (VortexStepMethod._PLOT_BACKEND[] = VortexStepMethod.ControlPlotsBackend()) -end - -""" - set_plot_style(titel_size=16; use_tex=false) - -Set the default style for plots using LaTeX. -`` -# Arguments: -- `titel_size`: size of the plot title in points (default: 16) -- `ùse_tex`: if the external `pdflatex` command shall be used -""" -function set_plot_style(titel_size=16; use_tex=false) - rcParams = plt.matplotlib."rcParams" - rcParams["text.usetex"] = use_tex - rcParams["font.family"] = "serif" - if use_tex - rcParams["font.serif"] = ["Computer Modern Roman"] - end - rcParams["axes.titlesize"] = titel_size - rcParams["axes.labelsize"] = 12 - rcParams["axes.linewidth"] = 1 - rcParams["lines.linewidth"] = 1 - rcParams["lines.markersize"] = 6 - rcParams["xtick.labelsize"] = 10 - rcParams["ytick.labelsize"] = 10 - rcParams["legend.fontsize"] = 10 - rcParams["figure.titlesize"] = 16 - if use_tex - rcParams["pgf.texsystem"] = "pdflatex" # Use pdflatex - end - rcParams["pgf.rcfonts"] = false - rcParams["figure.figsize"] = (10, 6) # Default figure size -end - - -""" - save_plot(fig, save_path, title; data_type=".pdf") - -Save a plot to a file. - -# Arguments -- `fig`: Plots figure object -- `save_path`: Path to save the plot -- `title`: Title of the plot - -# Keyword arguments -- `data_type`: File extension (default: ".pdf") -""" -function VortexStepMethod.save_plot(fig, save_path, title; data_type=".pdf") - isnothing(save_path) && throw(ArgumentError("save_path should be provided")) - - !isdir(save_path) && mkpath(save_path) - sanitized_title = replace(replace(String(title), ' ' => '_'), '%' => "pct") - full_path = joinpath(save_path, sanitized_title * data_type) - - @debug "Attempting to save figure to: $full_path" - @debug "Current working directory: $(pwd())" - - try - hasproperty(fig, :savefig) || throw(ArgumentError( - "Figure object of type $(typeof(fig)) does not support savefig()." - )) - getproperty(fig, :savefig)(full_path) - @debug "Figure saved as $data_type" - - if isfile(full_path) - @debug "File successfully saved to $full_path" - @debug "File size: $(filesize(full_path)) bytes" - else - @info "File does not exist after save attempt: $full_path" - end - catch e - @error "Error saving figure: $e" - @error "Error type: $(typeof(e))" - rethrow(e) - end -end - -""" - show_plot(fig::plt.Figure; dpi=130) - -Display a plot at specified DPI. - -# Arguments -- `fig`: Plots figure object - -# Keyword arguments -- `dpi`: Dots per inch for the figure (default: 130) -""" -function VortexStepMethod.show_plot(fig; dpi=130) - isnothing(fig) && throw(MethodError(VortexStepMethod.show_plot, (fig,))) - fig.set_dpi(dpi) - plt.display(fig) -end - -""" - plot_line_segment!(ax, segment, color, label; width=3) - -Plot a line segment in 3D with arrow. - -# Arguments -- `ax`: Plot axis -- `segment`: Array of two points defining the segment -- `color`: Color of the segment -- `label`: Label for the legend - -# Keyword Arguments -- `width`: Line width (default: 3) -""" -function plot_line_segment!(ax, segment, color, label; width=3) - ax.plot( - [segment[1][1], segment[2][1]], - [segment[1][2], segment[2][2]], - [segment[1][3], segment[2][3]], - color=color, label=label, linewidth=width - ) - - dir = segment[2] - segment[1] - ax.quiver( - [segment[1][1]], [segment[1][2]], [segment[1][3]], - [dir[1]], [dir[2]], [dir[3]], - color=color - ) -end - -""" - set_axes_equal!(ax; zoom=1.8) - -Set 3D plot axes to equal scale. - -# Arguments -- ax: 3D plot axis - -# Keyword arguments -zoom: zoom factor (default: 1.8) -""" -function set_axes_equal!(ax; zoom=1.8) - x_lims = pyconvert(Vector{Float64}, ax.get_xlim3d()) ./ zoom - y_lims = pyconvert(Vector{Float64}, ax.get_ylim3d()) ./ zoom - z_lims = pyconvert(Vector{Float64}, ax.get_zlim3d()) ./ zoom - - x_range = abs(x_lims[2] - x_lims[1]) - y_range = abs(y_lims[2] - y_lims[1]) - z_range = abs(z_lims[2] - z_lims[1]) - - max_range = max(x_range, y_range, z_range) - - x_mid = mean(x_lims) - y_mid = mean(y_lims) - z_mid = mean(z_lims) - - ax.set_xlim3d((x_mid - max_range / 2, x_mid + max_range / 2)) - ax.set_ylim3d((y_mid - max_range / 2, y_mid + max_range / 2)) - ax.set_zlim3d((z_mid - max_range / 2, z_mid + max_range / 2)) -end - -""" - create_geometry_plot(body_aero::BodyAerodynamics, title, view_elevation, view_azimuth; - zoom=1.8, use_tex=false) - -Create a 3D plot of wing geometry including panels and filaments. - -# Arguments -- body_aero: struct of type BodyAerodynamics -- title: plot title -- view_elevation: initial view elevation angle [°] -- view_azimuth: initial view azimuth angle [°] - -# Keyword arguments -- zoom: zoom factor (default: 1.8) -""" -function create_geometry_plot(body_aero::BodyAerodynamics, title, view_elevation, view_azimuth; - zoom=1.8, use_tex=false) - set_plot_style(28; use_tex) - - panels = body_aero.panels - isempty(panels) && throw(ArgumentError("Cannot plot geometry: body_aero.panels is empty.")) - - va = if body_aero.has_distributed_va - body_aero._va - else - isa(body_aero.va, Tuple) ? body_aero.va[1] : body_aero.va - end - - # Extract geometric data - control_points = [panel.control_point for panel in panels] - aero_centers = [panel.aero_center for panel in panels] - - # Create plot - fig = plt.figure(figsize=(14, 14)) - ax = fig.add_subplot(111, projection="3d") - ax.set_title(title) - - # Plot panels - legend_used = Dict{String,Bool}() - for (i, panel) in enumerate(panels) - # Plot panel edges and surfaces - corners = Matrix{Float64}(panel.corner_points) - x_corners = corners[1, :] - y_corners = corners[2, :] - z_corners = corners[3, :] - - push!(x_corners, x_corners[1]) - push!(y_corners, y_corners[1]) - push!(z_corners, z_corners[1]) - - ax.plot(x_corners, - y_corners, - z_corners, - color="grey", - linewidth=1, - label=i == 1 ? "Panel Edges" : "") - - # Plot control points and aerodynamic centers - ax.scatter([control_points[i][1]], [control_points[i][2]], [control_points[i][3]], - color="green", label=i == 1 ? "Control Points" : "") - ax.scatter([aero_centers[i][1]], [aero_centers[i][2]], [aero_centers[i][3]], - color="blue", label=i == 1 ? "Aerodynamic Centers" : "") - - # Plot filaments - filaments = calculate_filaments_for_plotting(panel) - legends = ["Bound Vortex", "side1", "side2", "wake_1", "wake_2"] - - for (filament, legend) in zip(filaments, legends) - x1, x2, color = filament - @debug "Legend: $legend" - show_legend = !get(legend_used, legend, false) - plot_line_segment!(ax, [x1, x2], color, show_legend ? legend : "") - legend_used[legend] = true - end - end - - # Plot velocity vector - max_chord = maximum(panel.chord for panel in panels) - va_mag = norm(va) - va_vector_begin = -2 * max_chord * va / va_mag - va_vector_end = va_vector_begin + 1.5 * va / va_mag - plot_line_segment!(ax, [va_vector_begin, va_vector_end], "lightblue", "va") - - # Add legends for the first occurrence of each label - # by_label = Dict(zip(labels, handles)) - # ax.legend(values(by_label), keys(by_label), bbox_to_anchor=(0, 0, 1.1, 1)) - - # Set labels and make axes equal - ax.set_xlabel("x") - ax.set_ylabel("y") - ax.set_zlabel("z") - set_axes_equal!(ax; zoom) - - # Set the initial view - ax.view_init(elev=view_elevation, azim=view_azimuth) - - # Ensure the figure is fully rendered - # fig.canvas.draw() - plt.tight_layout(rect=(0, 0, 1, 0.97)) - - return fig -end - -""" - plot_geometry(body_aero::BodyAerodynamics, title, ::ControlPlotsBackend; - data_type=".pdf", save_path=nothing, - is_save=false, is_show=false, - view_elevation=15, view_azimuth=-120, use_tex=false) - -ControlPlots backend implementation of [`plot_geometry`](@ref). - -# Arguments: -- `body_aero`: the [BodyAerodynamics](@ref) to plot -- `title`: plot title - -# Keyword arguments: -- `data_type``: string with the file type postfix (default: ".pdf") -- `save_path`: path for saving the graphic (default: `nothing`) -- `is_save`: boolean value, indicates if the graphic shall be saved (default: `false`) -- `is_show`: boolean value, indicates if the graphic shall be displayed (default: `false`) -- `view_elevation`: initial view elevation angle in degrees (default: 15) -- `view_azimuth`: initial view azimuth angle in degrees (default: -120) -- `use_tex`: if the external `pdflatex` command shall be used (default: false) - -""" -function VortexStepMethod.plot_geometry(body_aero::BodyAerodynamics, title, - ::VortexStepMethod.ControlPlotsBackend; - data_type=".pdf", - save_path=nothing, - is_save=false, - is_show=false, - view_elevation=15, - view_azimuth=-120, - use_tex=false) - - if is_save - plt.ioff() - # Angled view - fig = create_geometry_plot(body_aero, "$(title)_angled_view", 15, -120; use_tex) - save_plot(fig, save_path, "$(title)_angled_view", data_type=data_type) - - # Top view - fig = create_geometry_plot(body_aero, "$(title)_top_view", 90, 0; use_tex) - save_plot(fig, save_path, "$(title)_top_view", data_type=data_type) - - # Front view - fig = create_geometry_plot(body_aero, "$(title)_front_view", 0, 0; use_tex) - save_plot(fig, save_path, "$(title)_front_view", data_type=data_type) - - # Side view - fig = create_geometry_plot(body_aero, "$(title)_side_view", 0, -90; use_tex) - save_plot(fig, save_path, "$(title)_side_view", data_type=data_type) - end - - if is_show - plt.ion() - fig = create_geometry_plot(body_aero, title, view_elevation, view_azimuth; use_tex) - plt.display(fig) - else - fig = create_geometry_plot(body_aero, title, view_elevation, view_azimuth; use_tex) - end - fig -end - -""" - plot_distribution(y_coordinates_list, results_list, label_list, ::ControlPlotsBackend; - title="spanwise_distribution", data_type=".pdf", - save_path=nothing, is_save=false, is_show=true, use_tex=false) - -ControlPlots backend implementation of [`plot_distribution`](@ref). - -# Arguments -- `y_coordinates_list`: List of spanwise coordinates -- `results_list`: List of result dictionaries -- `label_list`: List of labels for different results - -# Keyword arguments -- `title`: Plot title (default: "spanwise_distribution") -- `data_type`: File extension for saving (default: ".pdf") -- `save_path`: Path to save plots (default: nothing) -- `is_save`: Whether to save plots (default: false) -- `is_show`: Whether to display plots (default: true) -- `use_tex`: if the external `pdflatex` command shall be used -""" -function VortexStepMethod.plot_distribution(y_coordinates_list, results_list, label_list, - ::VortexStepMethod.ControlPlotsBackend; - title="spanwise_distribution", - data_type=".pdf", - save_path=nothing, - is_save=false, - is_show=true, - use_tex=false) - - length(results_list) == length(label_list) || throw(ArgumentError( - "Number of results ($(length(results_list))) must match number of labels ($(length(label_list)))" - )) - - # Set the plot style - set_plot_style(; use_tex) - - # Initializing plot - fig, axs = plt.subplots(3, 3, figsize=(16, 10)) - fig.suptitle(title, fontsize=16) - - # CL plot - for (y_coordinates_i, result_i, label_i) in zip(y_coordinates_list, results_list, label_list) - value = "$(round(result_i["cl"], digits=2))" - if label_i == "LLT" - label = label_i * L" $~C_\mathrm{L}$: " * value - else - label = label_i * L" $C_\mathrm{L}$: " * value - end - axs[0, 0].plot( - y_coordinates_i, - result_i["cl_distribution"], - label=label - ) - end - axs[0, 0].set_title(L"$C_\mathrm{L}$ Distribution", size=16) - axs[0, 0].set_xlabel(L"Spanwise Position $y/b$") - axs[0, 0].set_ylabel(L"Lift Coefficient $C_\mathrm{L}$") - axs[0, 0].legend() - - # CD plot - for (y_coordinates_i, result_i, label_i) in zip(y_coordinates_list, results_list, label_list) - value = "$(round(result_i["cl"], digits=2))" - if label_i == "LLT" - label = label_i * L" $~C_\mathrm{D}$: " * value - else - label = label_i * L" $C_\mathrm{D}$: " * value - end - axs[0, 1].plot( - y_coordinates_i, - result_i["cd_distribution"], - label=label - ) - end - axs[0, 1].set_title(L"$C_\mathrm{D}$ Distribution", size=16) - axs[0, 1].set_xlabel(L"Spanwise Position $y/b$") - axs[0, 1].set_ylabel(L"Drag Coefficient $C_\mathrm{D}$") - axs[0, 1].legend() - - # Gamma Distribution - for (y_coordinates_i, result_i, label_i) in zip(y_coordinates_list, results_list, label_list) - axs[0, 2].plot( - y_coordinates_i, - result_i["gamma_distribution"], - label=label_i - ) - end - axs[0, 2].set_title(L"\Gamma~Distribution", size=16) - axs[0, 2].set_xlabel(L"Spanwise Position $y/b$") - axs[0, 2].set_ylabel(L"Circulation~\Gamma") - axs[0, 2].legend() - - # Geometric Alpha - for (y_coordinates_i, result_i, label_i) in zip(y_coordinates_list, results_list, label_list) - axs[1, 0].plot( - y_coordinates_i, - result_i["alpha_geometric"], - label=label_i - ) - end - axs[1, 0].set_title(L"$\alpha$ Geometric", size=16) - axs[1, 0].set_xlabel(L"Spanwise Position $y/b$") - axs[1, 0].set_ylabel(L"Angle of Attack $\alpha$ (deg)") - axs[1, 0].legend() - - # Calculated/ Corrected Alpha - for (y_coordinates_i, result_i, label_i) in zip(y_coordinates_list, results_list, label_list) - axs[1, 1].plot( - y_coordinates_i, - result_i["alpha_at_ac"], - label=label_i - ) - end - axs[1, 1].set_title(L"$\alpha$ result (corrected to aerodynamic center)", size=16) - axs[1, 1].set_xlabel(L"Spanwise Position $y/b$") - axs[1, 1].set_ylabel(L"Angle of Attack $\alpha$ (deg)") - axs[1, 1].legend() - - # Uncorrected Alpha plot - for (y_coordinates_i, result_i, label_i) in zip(y_coordinates_list, results_list, label_list) - axs[1, 2].plot( - y_coordinates_i, - result_i["alpha_uncorrected"], - label=label_i - ) - end - axs[1, 2].set_title(L"$\alpha$ Uncorrected (if VSM, at the control point)", size=16) - axs[1, 2].set_xlabel(L"Spanwise Position $y/b$") - axs[1, 2].set_ylabel(L"Angle of Attack $\alpha$ (deg)") - axs[1, 2].legend() - - # Force Components - for (idx, component) in enumerate(["x", "y", "z"]) - axs[2, idx-1].set_title("Force in $component direction", size=16) - axs[2, idx-1].set_xlabel(L"Spanwise Position $y/b$") - axs[2, idx-1].set_ylabel(raw"$F_\mathrm" * "{$component}" * raw"$") - for (y_coords, results, label) in zip(y_coordinates_list, results_list, label_list) - # Extract force components for the current direction (idx) - forces = results["F_distribution"][idx, :] - # Verify dimensions match - if length(y_coords) != length(forces) - @warn "Dimension mismatch in force plotting" length(y_coords) length(forces) component - continue # Skip this component instead of throwing error - end - space = "" - if label == "LLT" - space = "~" - end - axs[2, idx-1].plot( - y_coords, - forces, - label="$label" * space * raw"$~\Sigma~F_\mathrm" * "{$component}:~" * - raw"$" * "$(round(results["F$component"], digits=2)) N" - ) - axs[2, idx-1].legend() - end - end - - fig.tight_layout() - - # Save and show plot - if is_save - save_plot(fig, save_path, title, data_type=data_type) - end - - if is_show - show_plot(fig) - end - - return fig -end - -""" - generate_polar_data(solver, body_aero::BodyAerodynamics, angle_range; - angle_type="angle_of_attack", angle_of_attack=0.0, - side_slip=0.0, v_a=10.0, use_latex=false) - -Generate polar data for aerodynamic analysis over a range of angles. - -# Arguments -- `solver`: Aerodynamic solver object -- `body_aero`: Wing aerodynamics struct -- `angle_range`: Range of angles to analyze - -# Keyword arguments -- `angle_type`: Type of angle variation ("angle_of_attack" or "side_slip") -- `angle_of_attack`: Initial angle of attack [°] -- `side_slip`: Initial side slip angle [°] -- `v_a`: Norm of apparent wind speed [m/s] - -# Returns -- Tuple of polar data array and Reynolds number -""" - -""" - plot_polars(solver_list, body_aero_list, label_list, ::ControlPlotsBackend; - literature_path_list=String[], - angle_range=range(0, 20, 2), angle_type="angle_of_attack", - angle_of_attack=0.0, side_slip=0.0, v_a=10.0, - title="polar", data_type=".pdf", save_path=nothing, - is_save=true, is_show=true, use_tex=false) - -ControlPlots backend implementation of [`plot_polars`](@ref). - -# Arguments -- `solver_list`: List of aerodynamic solvers -- `body_aero_list`: List of wing aerodynamics objects -- `label_list`: List of labels for each configuration - -# Keyword arguments -- `literature_path_list`: Optional paths to literature data files -- `angle_range`: Range of angles to analyze in degrees -- `angle_type`: "`angle_of_attack`" or "`side_slip`"; (default: `angle_of_attack`) -- `angle_of_attack:` AoA to be used for plotting the polars (default: 0.0) [°] -- `side_slip`: side slip angle (default: 0.0) [°] -- `v_a`: norm of apparent wind speed (default: 10.0) [m/s] -- title: plot title -- `data_type`: File extension for saving (default: ".pdf") -- `save_path`: Path to save plots (default: nothing) -- `is_save`: Whether to save plots (default: true) -- `is_show`: Whether to display plots (default: true) -- `use_tex`: if the external `pdflatex` command shall be used (default: false) -- `cl_over_cd`: Plot CL/CD vs angle instead of CL vs CD (default: true) -""" -function VortexStepMethod.plot_polars( - solver_list, - body_aero_list, - label_list, - ::VortexStepMethod.ControlPlotsBackend; - literature_path_list=String[], - angle_range=range(0, 20, 2), - angle_type="angle_of_attack", - angle_of_attack=0.0, - side_slip=0.0, - v_a=10.0, - title="polar", - data_type=".pdf", - save_path=nothing, - is_save=true, - is_show=true, - use_tex=false, - cl_over_cd=true, - show_moments=false, -) - # Validate inputs - total_cases = length(body_aero_list) + length(literature_path_list) - if total_cases != length(label_list) || length(solver_list) != length(body_aero_list) - throw(ArgumentError("Mismatch in number of solvers ($(length(solver_list))), " * - "cases ($total_cases), and labels ($(length(label_list)))")) - end - main_title = replace(title, " " => "_") - set_plot_style(; use_tex) - - # Generate polar data - polar_data_list = [] - cm_data_list = [] - for (i, (solver, body_aero)) in enumerate(zip(solver_list, body_aero_list)) - result = VortexStepMethod.generate_polar_data( - solver, body_aero, angle_range; - angle_type, - angle_of_attack, - side_slip, - v_a - ) - push!(polar_data_list, result.polar_data) - push!(cm_data_list, (cmx=result.cmx, cmy=result.cmy, - cmz=result.cmz)) - # Update label with Reynolds number - label_list[i] = "$(label_list[i]) Re = $(round(Int64, result.rey*1e-5))e5" - end - # Load literature data if provided - if !isempty(literature_path_list) - for path in literature_path_list - raw_data = readdlm(path, ',') - lit = VortexStepMethod.extract_literature_polar_data( - raw_data, path; angle_type) - push!(polar_data_list, lit.polar_data) - push!(cm_data_list, (cmx=lit.cmx, cmy=lit.cmy, - cmz=lit.cmz)) - end - end - - # Number of computational results (excluding literature) - n_solvers = length(solver_list) - - # Helper: format label and line style - function format_label(label, i, n_solvers) - if i < n_solvers - linestyle, marker, markersize = "-", "*", 7 - else - linestyle, marker, markersize = "-", ".", 5 - end - if use_tex - if contains(label, "LLT") - label = replace(label, "e5" => raw"\cdot10^5") - label = replace(label, " " => raw"~") - label = replace(label, - "LLT" => raw"\mathrm{LLT}{~\,}") - label = raw"$" * label * raw"$" - else - label = replace(label, "e5" => raw"\cdot10^5") - label = replace(label, " " => "~") - label = replace(label, - "VSM" => raw"\mathrm{VSM}") - label = raw"$" * label * raw"$" - end - end - return label, linestyle, marker, markersize - end - - if show_moments - # 2x3 layout: CL, CD, CS, CMx, CMy, CMz - fig, axs = plt.subplots(2, 3, figsize=(21, 14)) - coeff_specs = [ - (1, raw"$C_\mathrm{L}$", 2, nothing), - (2, raw"$C_\mathrm{D}$", 3, nothing), - (3, raw"$C_\mathrm{S}$", 4, nothing), - (4, raw"$C_\mathrm{Mx}$", nothing, :cmx), - (5, raw"$C_\mathrm{My}$", nothing, :cmy), - (6, raw"$C_\mathrm{Mz}$", nothing, :cmz), - ] - for (ax_idx, ylabel, pd_col, cm_field) in coeff_specs - row = (ax_idx - 1) ÷ 3 - col = (ax_idx - 1) % 3 - ax = axs[row, col] - for (i, (polar_data, cm, label)) in enumerate( - zip(polar_data_list, cm_data_list, - label_list)) - label, ls, mk, ms = format_label( - label, i, n_solvers) - if pd_col !== nothing - y_data = polar_data[pd_col] - else - y_data = Float64.(getfield(cm, cm_field)) - all(isnan, y_data) && continue - end - ax.plot(polar_data[1], y_data; - label=label, linestyle=ls, - marker=mk, markersize=ms) - end - ax.set_title( - ylabel * " vs $angle_type [°]") - ax.set_xlabel("$angle_type [°]") - ax.set_ylabel(ylabel) - ax.legend() - end - else - # 2x2 layout: CL, CD, CS, CL/CD or CL-vs-CD - fig, axs = plt.subplots(2, 2, figsize=(14, 14)) - coeff_specs = [ - ((0, 0), raw"$C_\mathrm{L}$", 2), - ((0, 1), raw"$C_\mathrm{D}$", 3), - ((1, 0), raw"$C_\mathrm{S}$", 4), - ] - for (pos, ylabel, pd_col) in coeff_specs - ax = axs[pos...] - for (i, (polar_data, label)) in enumerate( - zip(polar_data_list, label_list)) - label, ls, mk, ms = format_label( - label, i, n_solvers) - ax.plot(polar_data[1], polar_data[pd_col]; - label=label, linestyle=ls, - marker=mk, markersize=ms) - if maximum(polar_data[2]) > 10 - ax.set_ylim([-0.5, 2]) - end - end - ax.set_title( - ylabel * " vs $angle_type [°]") - ax.set_xlabel("$angle_type [°]") - ax.set_ylabel(ylabel) - ax.legend() - end - # Fourth panel: CL/CD or CL-vs-CD - ax4 = axs[1, 1] - for (i, (polar_data, label)) in enumerate( - zip(polar_data_list, label_list)) - label, ls, mk, ms = format_label( - label, i, n_solvers) - if cl_over_cd - cl_cd = polar_data[2] ./ polar_data[3] - ax4.plot(polar_data[1], cl_cd; - label=label, linestyle=ls, - marker=mk, markersize=ms) - else - ax4.plot(polar_data[3], polar_data[2]; - label=label, linestyle=ls, - marker=mk, markersize=ms) - end - end - if cl_over_cd - ax4.set_title( - raw"$C_\mathrm{L}/C_\mathrm{D}$" * - " vs $angle_type [°]") - ax4.set_xlabel("$angle_type [°]") - ax4.set_ylabel( - raw"$C_\mathrm{L}/C_\mathrm{D}$") - else - ax4.set_title( - raw"$C_\mathrm{L}$" * " vs " * - raw"$C_\mathrm{D}$") - ax4.set_xlabel(raw"$C_\mathrm{D}$") - ax4.set_ylabel(raw"$C_\mathrm{L}$") - end - ax4.legend() - end - - fig.tight_layout(h_pad=3.5, rect=(0.01, 0.01, 0.99, 0.99)) - - # Save and show plot - if is_save && !isnothing(save_path) - save_plot(fig, save_path, main_title; data_type) - end - - if is_show - show_plot(fig) - end - - return fig -end - -""" - plot_polar_data(body_aero::BodyAerodynamics, ::ControlPlotsBackend; - alphas=collect(deg2rad.(-5:0.3:25)), - delta_tes=collect(deg2rad.(-5:0.3:25))) - -ControlPlots backend implementation of [`plot_polar_data`](@ref). Plots Cl, Cd, Cm as 3D surfaces -against angle of attack and trailing edge deflection angle. - -# Arguments -- `body_aero`: Wing aerodynamics struct - -# Keyword arguments -- `alphas`: Range of angle of attack values in radians (default: `deg2rad.(-5:0.3:25)`) -- `delta_tes`: Range of trailing edge angles in radians (default: `deg2rad.(-5:0.3:25)`) -- `is_show`: Whether to display plots (default: true) -- `use_tex`: if the external `pdflatex` command shall be used -""" -function VortexStepMethod.plot_polar_data(body_aero::BodyAerodynamics, - ::VortexStepMethod.ControlPlotsBackend; - alphas=collect(deg2rad.(-5:0.3:25)), - delta_tes = collect(deg2rad.(-5:0.3:25)), - is_show = true, - use_tex = false - ) - if body_aero.panels[1].aero_model == POLAR_MATRICES - set_plot_style(; use_tex) - - # Create figure with subplots - fig = plt.figure(figsize=(15, 6)) - - # Get interpolation functions and labels - interp_data = [ - (body_aero.panels[1].cl_interp, L"$C_l$"), - (body_aero.panels[1].cd_interp, L"$C_d$"), - (body_aero.panels[1].cm_interp, L"$C_m$") - ] - - # Create each subplot - for (idx, (interp, label)) in enumerate(interp_data) - ax = fig.add_subplot(1, 3, idx, projection="3d") - - # Create interpolation matrix - interp_matrix = zeros(length(delta_tes), length(alphas)) - interp_matrix .= [interp(alpha, delta_te) - for delta_te in delta_tes, alpha in alphas] - X = collect(delta_tes) .+ zeros(length(alphas))' - Y = collect(alphas)' .+ zeros(length(delta_tes)) - - # Plot surface - ax.plot_wireframe(X, Y, interp_matrix, - edgecolor="blue", - lw=0.5, - rstride=5, - cstride=5, - alpha=0.6) - - # Set labels and title - ax.set_xlabel(L"$\delta$ [rad]") - ax.set_ylabel(L"$\alpha$ [rad]") - ax.set_zlabel(label) - ax.set_title(label * L" vs $\alpha$ and $\delta$") - ax.grid(true) - end - - # Adjust layout and display - plt.tight_layout(rect=(0.01, 0.01, 0.99, 0.99)) - if is_show - show_plot(fig) - end - return fig - else - throw(ArgumentError("Plotting polar data for $(body_aero.panels[1].aero_model) is not implemented.")) - end -end - -""" - plot_combined_analysis(solver, body_aero, results, ::ControlPlotsBackend; kwargs...) - -ControlPlots backend implementation of [`plot_combined_analysis`](@ref). -Calls `plot_geometry`, `plot_distribution`, and `plot_polars` sequentially. - -# Arguments -- `solver`: Solver or array of solvers -- `body_aero`: BodyAerodynamics object or array -- `results`: Results dict or array of results dicts - -See individual functions for detailed parameter descriptions. -""" -function VortexStepMethod.plot_combined_analysis( - solver, - body_aero, - results, - backend::VortexStepMethod.ControlPlotsBackend; - solver_label="VSM", - labels=nothing, - angle_range=range(0, 20, length=20), - angle_type="angle_of_attack", - angle_of_attack=0.0, - side_slip=0.0, - v_a=10.0, - title="Combined Analysis", - data_type=".pdf", - save_path=nothing, - is_save=false, - is_show=true, - view_elevation=15, - view_azimuth=-120, - use_tex=false, - literature_path_list=String[], - cl_over_cd=true, - angle_of_attack_for_spanwise_distribution=5.0, -) - # Normalize inputs to arrays for consistent handling - solvers = solver isa Vector ? solver : [solver] - body_aeros = body_aero isa Vector ? body_aero : [body_aero] - results_list = results isa Vector ? results : [results] - n_solvers = length(solvers) - n_literature = length(literature_path_list) - - # Label normalization (matches Makie version) - label_source = isnothing(labels) ? solver_label : labels - labels_in = label_source isa AbstractVector ? - string.(label_source) : [string(label_source)] - solver_labels = length(labels_in) == 1 ? - fill(labels_in[1], n_solvers) : - labels_in[1:n_solvers] - - # Compute spanwise results at specified AoA - results_spanwise = copy(results_list) - if !isnothing(angle_of_attack_for_spanwise_distribution) - α_span = deg2rad( - angle_of_attack_for_spanwise_distribution) - β_span = deg2rad(side_slip) - for (i, (s, ba)) in enumerate( - zip(solvers, body_aeros)) - va_old = copy(getfield(ba, :_va)) - omega_old = copy(ba.omega) - set_va!(ba, [cos(α_span) * cos(β_span), - sin(β_span), sin(α_span)] * v_a) - results_spanwise[i] = solve(s, ba, - s.sol.gamma_distribution) - set_va!(ba, va_old, omega_old) - end - end - - # Extract y-coordinates for distribution plot - y_coords_list = [ - [p.aero_center[2] for p in ba.panels] - for ba in body_aeros] - - # Plot geometry (first body_aero only) - plot_geometry( - body_aeros[1], - title, - backend; - data_type, save_path, is_save, is_show, - view_elevation, view_azimuth, use_tex - ) - - # Plot spanwise distributions - plot_distribution( - y_coords_list, - results_spanwise, - solver_labels, - backend; - title=title * " - Distributions", - data_type, save_path, is_save, is_show, use_tex - ) - - # Plot polars (include literature labels) - polar_labels = if n_literature > 0 && - length(labels_in) == n_solvers + n_literature - labels_in - else - solver_labels - end - plot_polars( - solvers, - body_aeros, - polar_labels, - backend; - literature_path_list, angle_range, angle_type, - angle_of_attack, side_slip, v_a, - title=title * " - Polars", - data_type, save_path, is_save, is_show, - use_tex, cl_over_cd - ) -end - -end \ No newline at end of file diff --git a/ext/VortexStepMethodMakieExt.jl b/ext/VortexStepMethodMakieExt.jl index fce74bb1..2116ae37 100644 --- a/ext/VortexStepMethodMakieExt.jl +++ b/ext/VortexStepMethodMakieExt.jl @@ -1,5 +1,6 @@ module VortexStepMethodMakieExt using Makie, VortexStepMethod, LinearAlgebra, Statistics, DelimitedFiles +import MakieControlPlots import VortexStepMethod: calculate_filaments_for_plotting import VortexStepMethod: ObjAdapter, AirfoilAero @@ -7,12 +8,6 @@ export plot_geometry, plot_distribution, plot_polars, save_plot, show_plot, plot_polar_data, plot_combined_analysis, plot_section_polars -# Set this extension as the active plotting backend when loaded (only if not already set) -function __init__() - isnothing(VortexStepMethod._PLOT_BACKEND[]) && - (VortexStepMethod._PLOT_BACKEND[] = VortexStepMethod.MakieBackend()) -end - # Global storage for panel mesh observables (for dynamic plotting) const PANEL_MESH_OBSERVABLES = Ref{Union{Nothing,Dict}}(nothing) @@ -467,12 +462,12 @@ function create_geometry_plot_makie(body_aero::BodyAerodynamics, title, end """ - plot_geometry(body_aero::BodyAerodynamics, title, ::MakieBackend; + plot_geometry(body_aero::BodyAerodynamics, title; data_type=nothing, save_path=nothing, is_save=false, is_show=false, view_elevation=15, view_azimuth=-120, use_tex=false) -Makie backend implementation of [`plot_geometry`](@ref). +Makie implementation of [`plot_geometry`](@ref). # Arguments: - `body_aero`: the BodyAerodynamics to plot @@ -487,8 +482,7 @@ Makie backend implementation of [`plot_geometry`](@ref). - `view_azimuth`: View azimuth angle in degrees (default: -120) - `use_tex`: Ignored for Makie (default: false) """ -function VortexStepMethod.plot_geometry(body_aero::BodyAerodynamics, title, - ::VortexStepMethod.MakieBackend; +function VortexStepMethod.plot_geometry(body_aero::BodyAerodynamics, title; data_type=nothing, save_path=nothing, is_save=false, @@ -525,11 +519,11 @@ function VortexStepMethod.plot_geometry(body_aero::BodyAerodynamics, title, end """ - plot_distribution(y_coordinates_list, results_list, label_list, ::MakieBackend; + plot_distribution(y_coordinates_list, results_list, label_list; title="spanwise_distribution", data_type=nothing, save_path=nothing, is_save=false, is_show=true, use_tex=false) -Makie backend implementation of [`plot_distribution`](@ref). +Makie implementation of [`plot_distribution`](@ref). # Arguments - `y_coordinates_list`: List of spanwise coordinates @@ -544,8 +538,7 @@ Makie backend implementation of [`plot_distribution`](@ref). - `is_show`: Whether to display (default: true) - `use_tex`: Ignored for Makie (default: false) """ -function VortexStepMethod.plot_distribution(y_coordinates_list, results_list, label_list, - ::VortexStepMethod.MakieBackend; +function VortexStepMethod.plot_distribution(y_coordinates_list, results_list, label_list; title="spanwise_distribution", data_type=nothing, save_path=nothing, @@ -678,14 +671,14 @@ Generate polar data for aerodynamic analysis over a range of angles. """ """ - plot_polars(solver_list, body_aero_list, label_list, ::MakieBackend; + plot_polars(solver_list, body_aero_list, label_list; literature_path_list=String[], angle_range=range(0, 20, 2), angle_type="angle_of_attack", angle_of_attack=0.0, side_slip=0.0, v_a=10.0, title="polar", data_type=nothing, save_path=nothing, is_save=true, is_show=true, use_tex=false) -Makie backend implementation of [`plot_polars`](@ref). +Makie implementation of [`plot_polars`](@ref). # Arguments - `solver_list`: List of aerodynamic solvers @@ -710,8 +703,7 @@ Makie backend implementation of [`plot_polars`](@ref). function VortexStepMethod.plot_polars( solver_list, body_aero_list, - label_list, - ::VortexStepMethod.MakieBackend; + label_list; literature_path_list=String[], angle_range=range(0, 20, 2), angle_type="angle_of_attack", @@ -879,12 +871,12 @@ function VortexStepMethod.plot_polars( end """ - plot_polar_data(body_aero::BodyAerodynamics, ::MakieBackend; + plot_polar_data(body_aero::BodyAerodynamics; alphas=collect(deg2rad.(-5:0.3:25)), delta_tes=collect(deg2rad.(-5:0.3:25)), is_show=true, use_tex=false) -Makie backend implementation of [`plot_polar_data`](@ref). +Makie implementation of [`plot_polar_data`](@ref). # Arguments - `body_aero`: Wing aerodynamics struct @@ -895,8 +887,7 @@ Makie backend implementation of [`plot_polar_data`](@ref). - `is_show`: Whether to display (default: true) - `use_tex`: Ignored for Makie (default: false) """ -function VortexStepMethod.plot_polar_data(body_aero::BodyAerodynamics, - ::VortexStepMethod.MakieBackend; +function VortexStepMethod.plot_polar_data(body_aero::BodyAerodynamics; alphas=collect(deg2rad.(-5:0.3:25)), delta_tes=collect(deg2rad.(-5:0.3:25)), is_show=true, @@ -943,7 +934,7 @@ function VortexStepMethod.plot_polar_data(body_aero::BodyAerodynamics, end """ - plot_combined_analysis(solver, body_aero, results, ::MakieBackend; + plot_combined_analysis(solver, body_aero, results; solver_label="VSM", angle_range=range(0,20,length=20), angle_type="angle_of_attack", @@ -954,7 +945,7 @@ end literature_path_list=String[], data_type=".png", save_path=nothing, is_save=false) -Makie backend implementation of [`plot_combined_analysis`](@ref). +Makie implementation of [`plot_combined_analysis`](@ref). # Arguments - `solver`: Aerodynamic solver @@ -986,8 +977,7 @@ Makie backend implementation of [`plot_combined_analysis`](@ref). function VortexStepMethod.plot_combined_analysis( solver, body_aero, - results, - ::VortexStepMethod.MakieBackend; + results; solver_label="VSM", labels=nothing, angle_range=range(0, 20, length=20), @@ -1312,14 +1302,13 @@ function VortexStepMethod.plot_combined_analysis( end """ - plot_section_polars(body_aero, coefficient, ::MakieBackend; is_show=true, + plot_section_polars(body_aero, coefficient=:cl; is_show=true, is_save=false, save_path=nothing, data_type=".png") -Makie backend implementation of [`plot_section_polars`](@ref). +Implementation of [`plot_section_polars`](@ref); rendered through `MakieControlPlots`. """ function VortexStepMethod.plot_section_polars(body_aero::BodyAerodynamics, - coefficient::Symbol, ::VortexStepMethod.MakieBackend; - is_show::Bool=true, is_save::Bool=false, + coefficient::Symbol=:cl; is_show::Bool=true, is_save::Bool=false, save_path=nothing, data_type::String=".png") coefficient in (:cl, :cd, :cm) || @@ -1327,27 +1316,36 @@ function VortexStepMethod.plot_section_polars(body_aero::BodyAerodynamics, idx = coefficient === :cl ? 2 : coefficient === :cd ? 3 : 4 label = uppercasefirst(string(coefficient)) - fig = Figure(size=(900, 600)) - ax = Axis(fig[1, 1]; title="$label per section", xlabel="α [deg]", ylabel=label) - - n_sections = 0 + alphas_deg = nothing + series = Vector{Float64}[] + labels = String[] for wing in body_aero.wings for (s, section) in enumerate(wing.unrefined_sections) section.aero_model == POLAR_VECTORS || continue aero = section.aero_data aero === nothing && continue - lines!(ax, rad2deg.(aero[1]), aero[idx]; label="section $s") - n_sections += 1 + section_alphas = rad2deg.(aero[1]) + if isnothing(alphas_deg) + alphas_deg = collect(section_alphas) + elseif length(section_alphas) != length(alphas_deg) + @warn "section $s has a different α grid; plotting against the first section's α" + end + push!(series, Float64.(aero[idx])) + push!(labels, "section $s") end end - n_sections == 0 && error("No POLAR_VECTORS sections found in body") - n_sections <= 12 && axislegend(ax; position=:rt, framevisible=false) + isempty(series) && error("No POLAR_VECTORS sections found in body") + + plt = MakieControlPlots.plot(alphas_deg, series; + xlabel="α [deg]", ylabel=label, title="$label per section", + labels=labels, disp=(is_show || is_save)) if is_save && !isnothing(save_path) - VortexStepMethod.save_plot(fig, save_path, "section_polars_$coefficient"; data_type) + isdir(save_path) || mkpath(save_path) + MakieControlPlots.savefig( + joinpath(save_path, "section_polars_$(coefficient)$(data_type)")) end - is_show && display(fig) - return fig + return plt end # --- OBJ mesh / airfoil plotting (formerly ObjAdapterMakieExt) --- @@ -1622,11 +1620,11 @@ function ObjAdapter.plot_airfoil_fit(x::Vector, y::Vector; title::String="Airfoi end """ - plot_airfoils(geometry_file, ::MakieBackend; overlay=nothing, symmetric=false, + plot_airfoils(geometry_file; overlay=nothing, symmetric=false, idxs=nothing, n_cols=3, is_show=true, is_save=false, save_path=nothing, data_type=".png") -Makie backend implementation of [`plot_airfoils`](@ref). +Makie implementation of [`plot_airfoils`](@ref). With `overlay=false` each airfoil gets its own subplot; with `overlay=true` all airfoils are drawn on one axis coloured by section. By default (`overlay=nothing`) @@ -1636,8 +1634,7 @@ the fitted airfoil as a line. Pass `idxs` (e.g. `idxs=[1]`) to plot only those airfoils by position; otherwise `symmetric=true` shows just the first half of the sections (the wing is mirror-symmetric, so the other half is redundant). """ -function ObjAdapter.plot_airfoils(geometry_file::String, - ::VortexStepMethod.MakieBackend; +function ObjAdapter.plot_airfoils(geometry_file::String; overlay=nothing, symmetric::Bool=false, idxs=nothing, n_cols::Int=3, is_show::Bool=true, is_save::Bool=false, save_path=nothing, data_type::String=".png") diff --git a/src/VortexStepMethod.jl b/src/VortexStepMethod.jl index 4dea64bf..2df50a85 100644 --- a/src/VortexStepMethod.jl +++ b/src/VortexStepMethod.jl @@ -46,68 +46,24 @@ export load_polar_data export CpData, CpPolar, cp_distribution, delta_cp export read_cp_data, write_cp_data -export plot_circulation_distribution, - plot_combined_analysis, plot_distribution, plot_geometry, plot_polar_data, +export plot_combined_analysis, plot_distribution, plot_geometry, plot_polar_data, plot_polars, plot_section_polars, save_plot, show_plot -# Backend dispatch types for multi-backend support (Makie and ControlPlots can coexist) -abstract type PlotBackend end -struct MakieBackend <: PlotBackend end -struct ControlPlotsBackend <: PlotBackend end -export PlotBackend, MakieBackend, ControlPlotsBackend - -const _PLOT_BACKEND = Ref{Union{Nothing, PlotBackend}}(nothing) - -""" - set_plot_backend!(backend::PlotBackend) - -Select the active plotting backend when both Makie and ControlPlots are loaded. - -# Example -```julia -set_plot_backend!(MakieBackend()) -set_plot_backend!(ControlPlotsBackend()) -``` -""" -function set_plot_backend!(backend::PlotBackend) - _PLOT_BACKEND[] = backend -end -export set_plot_backend! - -# Generic stubs — extended by MakieExt and ControlPlotsExt with a PlotBackend argument. -# The no-backend-argument wrappers below route through the active backend. -function plot_geometry end -function plot_distribution end -function plot_circulation_distribution end -function plot_polars end -function save_plot end -function show_plot end -function plot_polar_data end -function plot_combined_analysis end -function plot_section_polars end - -function _active_backend() - b = _PLOT_BACKEND[] - isnothing(b) && error( - "No plotting backend loaded. Load Makie or ControlPlots first, " * - "or call set_plot_backend!(MakieBackend()) / set_plot_backend!(ControlPlotsBackend()) " * - "when both are loaded." - ) - b -end +# Plotting functions live in the `VortexStepMethodMakieExt` extension, loaded once a +# Makie backend (`GLMakie` or `CairoMakie`) and `MakieControlPlots` are available. The +# declarations below carry the public docstrings; the extension provides the methods. """ plot_geometry(body_aero::BodyAerodynamics, title; kwargs...) Plot wing geometry from different viewpoints and optionally save/show plots. -Routes to the active plotting backend (Makie or ControlPlots). # Arguments - `body_aero`: the [`BodyAerodynamics`](@ref) to plot - `title`: plot title # Keyword arguments -- `data_type`: file extension for saving (default depends on backend) +- `data_type`: file extension for saving (default: `".png"`) - `save_path`: path for saving the graphic (default: `nothing`) - `is_save`: whether to save the graphic (default: `false`) - `is_show`: whether to display the graphic (default: `false`) @@ -115,15 +71,12 @@ Routes to the active plotting backend (Makie or ControlPlots). - `view_azimuth`: initial view azimuth angle in degrees (default: `-120`) - `use_tex`: use external `pdflatex` for rendering (default: `false`; ignored by Makie) """ -function plot_geometry(body_aero, title; kwargs...) - plot_geometry(body_aero, title, _active_backend(); kwargs...) -end +function plot_geometry end """ plot_distribution(y_coordinates_list, results_list, label_list; kwargs...) Plot spanwise distributions of aerodynamic properties. -Routes to the active plotting backend (Makie or ControlPlots). # Arguments - `y_coordinates_list`: list of spanwise coordinate arrays @@ -132,21 +85,18 @@ Routes to the active plotting backend (Makie or ControlPlots). # Keyword arguments - `title`: plot title (default: `"spanwise_distribution"`) -- `data_type`: file extension for saving (default depends on backend) +- `data_type`: file extension for saving (default: `".png"`) - `save_path`: path to save plots (default: `nothing`) - `is_save`: whether to save (default: `false`) - `is_show`: whether to display (default: `true`) - `use_tex`: use external `pdflatex` for rendering (default: `false`; ignored by Makie) """ -function plot_distribution(y_coordinates_list, results_list, label_list; kwargs...) - plot_distribution(y_coordinates_list, results_list, label_list, _active_backend(); kwargs...) -end +function plot_distribution end """ plot_polars(solver_list, body_aero_list, label_list; kwargs...) Plot polar data comparing different solvers and configurations. -Routes to the active plotting backend (Makie or ControlPlots). # Arguments - `solver_list`: list of aerodynamic solvers @@ -161,22 +111,19 @@ Routes to the active plotting backend (Makie or ControlPlots). - `side_slip`: side slip angle (default: `0.0`) [°] - `v_a`: apparent wind speed magnitude (default: `10.0`) [m/s] - `title`: plot title (default: `"polar"`) -- `data_type`: file extension for saving (default depends on backend) +- `data_type`: file extension for saving (default: `".png"`) - `save_path`: path to save plots (default: `nothing`) - `is_save`: whether to save (default: `true`) - `is_show`: whether to display (default: `true`) - `use_tex`: use external `pdflatex` for rendering (default: `false`; ignored by Makie) - `cl_over_cd`: plot CL/CD vs angle instead of CL vs CD (default: `true`) """ -function plot_polars(solver_list, body_aero_list, label_list; kwargs...) - plot_polars(solver_list, body_aero_list, label_list, _active_backend(); kwargs...) -end +function plot_polars end """ plot_polar_data(body_aero::BodyAerodynamics; kwargs...) Plot polar data (Cl, Cd, Cm) as 3-D surfaces against angle of attack and trailing edge deflection. -Routes to the active plotting backend (Makie or ControlPlots). # Arguments - `body_aero`: the [`BodyAerodynamics`](@ref) to plot (must use `POLAR_MATRICES` aero model) @@ -187,15 +134,13 @@ Routes to the active plotting backend (Makie or ControlPlots). - `is_show`: whether to display (default: `true`) - `use_tex`: use external `pdflatex` for rendering (default: `false`; ignored by Makie) """ -function plot_polar_data(body_aero; kwargs...) - plot_polar_data(body_aero, _active_backend(); kwargs...) -end +function plot_polar_data end """ plot_combined_analysis(solver, body_aero, results; kwargs...) Create a combined analysis by calling `plot_geometry`, `plot_distribution`, and `plot_polars` -in sequence. Routes to the active plotting backend (Makie or ControlPlots). +in sequence. # Arguments - `solver`: solver or vector of solvers @@ -216,21 +161,19 @@ in sequence. Routes to the active plotting backend (Makie or ControlPlots). - `is_show`: whether to display (default: `true`) - `use_tex`: use external `pdflatex` for rendering (default: `false`; ignored by Makie) - `literature_path_list`: paths to literature CSV files (default: `String[]`) -- `data_type`: file extension for saving (default depends on backend) +- `data_type`: file extension for saving (default: `".png"`) - `save_path`: directory to save files (default: `nothing`) - `is_save`: whether to save (default: `false`) - `cl_over_cd`: plot CL/CD vs angle (default: `true`) """ -function plot_combined_analysis(solver, body_aero, results; kwargs...) - plot_combined_analysis(solver, body_aero, results, _active_backend(); kwargs...) -end +function plot_combined_analysis end """ plot_section_polars(body_aero::BodyAerodynamics, coefficient=:cl; kwargs...) Plot one polar coefficient (`:cl`, `:cd`, or `:cm`) against angle of attack for -every section of a wing using stored `POLAR_VECTORS` data. Routes to the active -plotting backend. +every section of a wing using stored `POLAR_VECTORS` data. Rendered through +`MakieControlPlots`. # Arguments - `body_aero`: the [`BodyAerodynamics`](@ref) to plot @@ -242,9 +185,10 @@ plotting backend. - `save_path`: directory to save the figure (default: `nothing`) - `data_type`: file extension for saving (default: `".png"`) """ -function plot_section_polars(body_aero, coefficient::Symbol=:cl; kwargs...) - plot_section_polars(body_aero, coefficient, _active_backend(); kwargs...) -end +function plot_section_polars end + +function save_plot end +function show_plot end """ const MVec3 = MVector{3, Float64} diff --git a/src/obj_adapter/ObjAdapter.jl b/src/obj_adapter/ObjAdapter.jl index 4b4848fb..698ca9a0 100644 --- a/src/obj_adapter/ObjAdapter.jl +++ b/src/obj_adapter/ObjAdapter.jl @@ -28,13 +28,10 @@ function plot_airfoil_fit end plot_airfoils(geometry_file::String; kwargs...) Plot every airfoil of a YAML wing geometry. The airfoil shapes are read from the -`.dat` files referenced by the geometry's `wing_airfoils`. Routes to the active -plotting backend; implemented in `ObjAdapterMakieExt` (load `Makie`/`GLMakie`). +`.dat` files referenced by the geometry's `wing_airfoils`. Implemented in the +`VortexStepMethodMakieExt` extension (load a Makie backend, e.g. `GLMakie`). """ function plot_airfoils end -function plot_airfoils(geometry_file::String; kwargs...) - plot_airfoils(geometry_file, VortexStepMethod._active_backend(); kwargs...) -end """ plot_slices_3d(path; n_slices=10, rotation=I, delta=0.0, is_show=true) diff --git a/test/Aqua.jl b/test/Aqua.jl index 4ed9be3c..2c2f3755 100644 --- a/test/Aqua.jl +++ b/test/Aqua.jl @@ -2,8 +2,7 @@ using Aqua, VortexStepMethod, Test @testset "Aqua.jl" begin Aqua.test_all( VortexStepMethod; - stale_deps=(ignore=[:Xfoil, :Timers, :PyCall],), - deps_compat=(ignore=[:PyCall],), + stale_deps=(ignore=[:Xfoil, :Timers],), persistent_tasks=false ) end diff --git a/test/LocalPreferences.toml.default b/test/LocalPreferences.toml.default deleted file mode 100644 index a2ee2658..00000000 --- a/test/LocalPreferences.toml.default +++ /dev/null @@ -1,2 +0,0 @@ -[CondaPkg] -backend = "Null" diff --git a/test/Project.toml b/test/Project.toml index e81a7223..5e1ffca3 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -3,7 +3,6 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" -ControlPlots = "23c2ee80-7a9e-4350-b264-8e670f12517c" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" @@ -12,8 +11,8 @@ ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" +MakieControlPlots = "6d616b69-6563-4f6e-8472-6f6c706c6f74" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" @@ -30,13 +29,12 @@ Aqua = "0.8" BenchmarkTools = "1" CSV = "0.10" CairoMakie = "0" -ControlPlots = "0.2.5, 0.3" DataFrames = "1.7" Documenter = "1.8" Interpolations = "0.15, 0.16" LinearAlgebra = "1" Logging = "1" -PythonCall = "0.9" +MakieControlPlots = "0.1.5" Random = "1.10.0" Serialization = "1" StaticArrays = "1" diff --git a/test/plotting/test_backend_coexistence.jl b/test/plotting/test_backend_coexistence.jl deleted file mode 100644 index 40aaaf90..00000000 --- a/test/plotting/test_backend_coexistence.jl +++ /dev/null @@ -1,38 +0,0 @@ -# Regression test for https://github.com/OpenSourceAWE/VortexStepMethod.jl/issues/236 -# Verifies that loading both Makie and ControlPlots in the same process: -# (1) does not cause method-redefinition / precompile errors, and -# (2) set_plot_backend! correctly switches which backend the no-backend wrappers route to. - -using CairoMakie -using ControlPlots - -@testset "Backend coexistence (Makie + ControlPlots)" begin - backend_before = VortexStepMethod._PLOT_BACKEND[] - try - # (1) Both extensions must be loaded without errors when both packages are in scope. - # If either `using` above threw a method-redefinition error we would never reach here. - makie_ext = Base.get_extension(VortexStepMethod, :VortexStepMethodMakieExt) - cp_ext = Base.get_extension(VortexStepMethod, :VortexStepMethodControlPlotsExt) - @test makie_ext !== nothing - - if cp_ext === nothing - @test_skip "VortexStepMethodControlPlotsExt unavailable (ControlPlots failed to load)" - return - end - @test cp_ext !== nothing - - # (2) set_plot_backend! correctly switches the active backend. - set_plot_backend!(ControlPlotsBackend()) - @test VortexStepMethod._PLOT_BACKEND[] isa VortexStepMethod.ControlPlotsBackend - - set_plot_backend!(MakieBackend()) - @test VortexStepMethod._PLOT_BACKEND[] isa VortexStepMethod.MakieBackend - - # Round-trip: switch back to ControlPlots. - set_plot_backend!(ControlPlotsBackend()) - @test VortexStepMethod._PLOT_BACKEND[] isa VortexStepMethod.ControlPlotsBackend - finally - # Restore whatever backend was active before this testset ran. - VortexStepMethod._PLOT_BACKEND[] = backend_before - end -end diff --git a/test/plotting/test_plotting.jl b/test/plotting/test_plotting.jl index 87aa3c68..9890db45 100644 --- a/test/plotting/test_plotting.jl +++ b/test/plotting/test_plotting.jl @@ -13,21 +13,16 @@ module FakeMakieReturnsOtherModule import Base current_backend() = Base end -backend = if "plot-controlplots" in ARGS - using ControlPlots - import ControlPlots: plt - using PythonCall: pyconvert - "ControlPlots" -else - using CairoMakie - "Makie" -end +using CairoMakie +using MakieControlPlots +# MakieControlPlots activates GLMakie on load; force CairoMakie so headless +# figure saving uses the software backend. +CairoMakie.activate!() using VortexStepMethod using Test -const makie_ext = backend == "Makie" ? - Base.get_extension(VortexStepMethod, :VortexStepMethodMakieExt) : nothing +const makie_ext = Base.get_extension(VortexStepMethod, :VortexStepMethodMakieExt) # Helper to robustly delete files on platforms with occasional file locks safe_rm(path) = begin @@ -76,7 +71,7 @@ function create_body_aero() body_aero end -@testset "Plotting ($backend)" begin +@testset "Plotting (Makie)" begin save_dir = tempdir() body_aero = create_body_aero() @@ -87,21 +82,11 @@ end save_path=save_dir, is_save=true, is_show=false) - if backend == "Makie" - @test fig isa Figure - else - @test fig !== nothing - end + @test fig isa Figure - if backend == "Makie" - @test hasmethod(VortexStepMethod.show_plot, Tuple{Figure}) - @test_throws MethodError VortexStepMethod.show_plot(nothing) - @test_nowarn VortexStepMethod.show_plot(fig) - else - FigType = plt.Figure - @test hasmethod(VortexStepMethod.show_plot, Tuple{FigType}) - @test_throws MethodError VortexStepMethod.show_plot(nothing) - end + @test hasmethod(VortexStepMethod.show_plot, Tuple{Figure}) + @test_throws MethodError VortexStepMethod.show_plot(nothing) + @test_nowarn VortexStepMethod.show_plot(fig) @test isfile(joinpath(save_dir, "Rectangular_wing_geometry_angled_view.png")) @@ -139,11 +124,7 @@ end title="Spanwise Distributions", is_show=false ) - if backend == "Makie" - @test fig isa Figure - else - @test fig !== nothing - end + @test fig isa Figure # Plot polar curves v_a = 20.0 @@ -161,11 +142,7 @@ end is_save=true, is_show=false ) - if backend == "Makie" - @test fig isa Figure - else - @test fig !== nothing - end + @test fig isa Figure @test isfile(joinpath(save_dir, "Rectangular_Wing_Polars.png")) safe_rm(joinpath(save_dir, "Rectangular_Wing_Polars.png")) @@ -182,11 +159,7 @@ end is_show=false, cl_over_cd=false ) - if backend == "Makie" - @test fig isa Figure - else - @test fig !== nothing - end + @test fig isa Figure # Plot combined analysis with cl_over_cd fig = plot_combined_analysis( @@ -200,11 +173,7 @@ end is_show=false, cl_over_cd=true ) - if backend == "Makie" - @test fig isa Figure - else - @test fig !== nothing - end + @test fig isa Figure # Plot combined analysis with cl_over_cd=false fig = plot_combined_analysis( @@ -218,30 +187,18 @@ end is_show=false, cl_over_cd=false ) - if backend == "Makie" - @test fig isa Figure - else - @test fig !== nothing - end + @test fig isa Figure # Test polar data plotting body_aero = BodyAerodynamics([ram_wing]) fig = plot_polar_data(body_aero; is_show=false) - if backend == "Makie" - @test fig isa Figure - else - @test fig !== nothing - end + @test fig isa Figure fig_rect = plot_polar_data(body_aero; alphas=collect(deg2rad.(-5:1.0:15)), delta_tes=collect(deg2rad.(-3:1.0:5)), is_show=false) - if backend == "Makie" - @test fig_rect isa Figure - else - @test fig_rect !== nothing - end + @test fig_rect isa Figure # Tests for both backends body_aero_empty = create_body_aero() @@ -290,25 +247,7 @@ end safe_rm(literature_csv) end - # CP-specific tests (DPI, matplotlib internals) - if backend == "ControlPlots" - # `get_dpi()` returns a Python float; convert to a Julia Float64 so the - # comparisons below evaluate to a Julia `Bool` rather than a `Py` object. - get_dpi(fig) = pyconvert(Float64, fig.get_dpi()) - - fig_dpi = plt.figure() - default_dpi = get_dpi(fig_dpi) - @test default_dpi != 173 - - show_plot(fig_dpi; dpi=173) - @test get_dpi(fig_dpi) == 173 - - show_plot(fig_dpi) - @test get_dpi(fig_dpi) == 130 - plt.close(fig_dpi) - end - - # Unit tests for shared extract_literature_polar_data (both backends) + # Unit tests for shared extract_literature_polar_data using DelimitedFiles # Tuple parsing branch (e.g. readdlm(...; header=true) shape) @@ -446,59 +385,57 @@ end safe_rm(no_cm_path) # Tests for save_plot function - if backend == "Makie" - @testset "_active_backend_prefers_vector_output" begin - @test makie_ext !== nothing + @testset "_active_backend_prefers_vector_output" begin + @test makie_ext !== nothing - active_backend_prefers_vector_output = - getfield(makie_ext, :_active_backend_prefers_vector_output) + active_backend_prefers_vector_output = + getfield(makie_ext, :_active_backend_prefers_vector_output) - @test active_backend_prefers_vector_output(FakeMakieNoCurrentBackend) == false - @test active_backend_prefers_vector_output(FakeMakieCurrentBackendThrows) == false + @test active_backend_prefers_vector_output(FakeMakieNoCurrentBackend) == false + @test active_backend_prefers_vector_output(FakeMakieCurrentBackendThrows) == false - @test active_backend_prefers_vector_output(FakeMakieReturnsCairoModule) == true - @test active_backend_prefers_vector_output(FakeMakieReturnsOtherModule) == false - end + @test active_backend_prefers_vector_output(FakeMakieReturnsCairoModule) == true + @test active_backend_prefers_vector_output(FakeMakieReturnsOtherModule) == false + end - body_aero = create_body_aero() - fig = plot_geometry( - body_aero, - "save_plot_test"; - is_save=false, - is_show=false) - @test fig isa Figure + body_aero = create_body_aero() + fig = plot_geometry( + body_aero, + "save_plot_test"; + is_save=false, + is_show=false) + @test fig isa Figure - active_backend_prefers_vector_output = - getfield(makie_ext, :_active_backend_prefers_vector_output) + active_backend_prefers_vector_output = + getfield(makie_ext, :_active_backend_prefers_vector_output) - save_test_dir = tempdir() - - # Test 1: save_plot with explicit data_type (".png") - VortexStepMethod.save_plot(fig, save_test_dir, "test_explicit_png", data_type=".png") - @test isfile(joinpath(save_test_dir, "test_explicit_png.png")) - safe_rm(joinpath(save_test_dir, "test_explicit_png.png")) + save_test_dir = tempdir() + + # Test 1: save_plot with explicit data_type (".png") + VortexStepMethod.save_plot(fig, save_test_dir, "test_explicit_png", data_type=".png") + @test isfile(joinpath(save_test_dir, "test_explicit_png.png")) + safe_rm(joinpath(save_test_dir, "test_explicit_png.png")) - # Test 2: save_plot with explicit data_type (".pdf") - VortexStepMethod.save_plot(fig, save_test_dir, "test_explicit_pdf", data_type=".pdf") - @test isfile(joinpath(save_test_dir, "test_explicit_pdf.pdf")) - safe_rm(joinpath(save_test_dir, "test_explicit_pdf.pdf")) + # Test 2: save_plot with explicit data_type (".pdf") + VortexStepMethod.save_plot(fig, save_test_dir, "test_explicit_pdf", data_type=".pdf") + @test isfile(joinpath(save_test_dir, "test_explicit_pdf.pdf")) + safe_rm(joinpath(save_test_dir, "test_explicit_pdf.pdf")) - # Test 3: save_plot with data_type=nothing (backend-aware detection) - backend_aware_dir = mktempdir() - try - VortexStepMethod.save_plot(fig, backend_aware_dir, "test_backend_aware", data_type=nothing) - pdf_path = joinpath(backend_aware_dir, "test_backend_aware.pdf") - png_path = joinpath(backend_aware_dir, "test_backend_aware.png") - expected_ext = active_backend_prefers_vector_output(Makie) ? ".pdf" : ".png" - - @test xor(isfile(pdf_path), isfile(png_path)) - @test isfile(joinpath(backend_aware_dir, "test_backend_aware" * expected_ext)) - finally - safe_rm(joinpath(backend_aware_dir, "test_backend_aware.pdf")) - safe_rm(joinpath(backend_aware_dir, "test_backend_aware.png")) - rm(backend_aware_dir; force=true, recursive=true) - end + # Test 3: save_plot with data_type=nothing (backend-aware detection) + backend_aware_dir = mktempdir() + try + VortexStepMethod.save_plot(fig, backend_aware_dir, "test_backend_aware", data_type=nothing) + pdf_path = joinpath(backend_aware_dir, "test_backend_aware.pdf") + png_path = joinpath(backend_aware_dir, "test_backend_aware.png") + expected_ext = active_backend_prefers_vector_output(Makie) ? ".pdf" : ".png" + + @test xor(isfile(pdf_path), isfile(png_path)) + @test isfile(joinpath(backend_aware_dir, "test_backend_aware" * expected_ext)) + finally + safe_rm(joinpath(backend_aware_dir, "test_backend_aware.pdf")) + safe_rm(joinpath(backend_aware_dir, "test_backend_aware.png")) + rm(backend_aware_dir; force=true, recursive=true) # Test 4: save_plot with title containing spaces (should be sanitized to underscores) VortexStepMethod.save_plot(fig, save_test_dir, "test with spaces", data_type=".png") diff --git a/test/runtests.jl b/test/runtests.jl index b15566e5..69ee3153 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,13 +4,10 @@ using Test, VortexStepMethod cd(@__DIR__) # ensure we're in test/ no matter how tests are launched include("test_data_utils.jl") -# Filter special args from pattern matching -const test_patterns = filter(a -> a != "plot-controlplots", ARGS) +const test_patterns = ARGS println("Running tests...") -if "plot-controlplots" in ARGS - println("Running plotting tests with ControlPlots backend") -elseif !isempty(test_patterns) +if !isempty(test_patterns) println("Filtering tests matching: ", test_patterns) end @@ -34,35 +31,30 @@ const build_is_production_build = let v = get(ENV, build_is_production_build_env end::Bool function include_selected_tests() - if "plot-controlplots" in ARGS - include("plotting/test_plotting.jl") - else - if build_is_production_build && should_run_test("bench") - include("bench.jl") - end - should_run_test("body_aerodynamics/test_body_aerodynamics.jl") && include("body_aerodynamics/test_body_aerodynamics.jl") - should_run_test("body_aerodynamics/test_results.jl") && include("body_aerodynamics/test_results.jl") - should_run_test("test_refinement_validation.jl") && include("test_refinement_validation.jl") - should_run_test("filament/test_bound_filament.jl") && include("filament/test_bound_filament.jl") - should_run_test("filament/test_semi_infinite_filament.jl") && include("filament/test_semi_infinite_filament.jl") - should_run_test("panel/test_panel.jl") && include("panel/test_panel.jl") - should_run_test("plotting/test_plotting.jl") && include("plotting/test_plotting.jl") - ("plot-controlplots" in ARGS) && should_run_test("plotting/test_backend_coexistence.jl") && include("plotting/test_backend_coexistence.jl") - should_run_test("ram_geometry/test_kite_geometry.jl") && include("ram_geometry/test_kite_geometry.jl") - should_run_test("settings/test_settings.jl") && include("settings/test_settings.jl") - should_run_test("solver/test_solver.jl") && include("solver/test_solver.jl") - should_run_test("solver/test_forwarddiff.jl") && include("solver/test_forwarddiff.jl") - should_run_test("solver/test_backend_comparison.jl") && include("solver/test_backend_comparison.jl") - should_run_test("solver/test_unrefined_dist.jl") && include("solver/test_unrefined_dist.jl") - should_run_test("VortexStepMethod/test_VortexStepMethod.jl") && include("VortexStepMethod/test_VortexStepMethod.jl") - should_run_test("wake/test_wake.jl") && include("wake/test_wake.jl") - should_run_test("wing_geometry/test_wing_geometry.jl") && include("wing_geometry/test_wing_geometry.jl") - should_run_test("wing_geometry/test_billowing.jl") && include("wing_geometry/test_billowing.jl") - should_run_test("yaml_geometry/test_yaml_geometry.jl") && include("yaml_geometry/test_yaml_geometry.jl") - should_run_test("airfoil_aero/test_airfoil_aero.jl") && include("airfoil_aero/test_airfoil_aero.jl") - should_run_test("obj_adapter/test_obj_adapter.jl") && include("obj_adapter/test_obj_adapter.jl") - should_run_test("Aqua.jl") && include("Aqua.jl") + if build_is_production_build && should_run_test("bench") + include("bench.jl") end + should_run_test("body_aerodynamics/test_body_aerodynamics.jl") && include("body_aerodynamics/test_body_aerodynamics.jl") + should_run_test("body_aerodynamics/test_results.jl") && include("body_aerodynamics/test_results.jl") + should_run_test("test_refinement_validation.jl") && include("test_refinement_validation.jl") + should_run_test("filament/test_bound_filament.jl") && include("filament/test_bound_filament.jl") + should_run_test("filament/test_semi_infinite_filament.jl") && include("filament/test_semi_infinite_filament.jl") + should_run_test("panel/test_panel.jl") && include("panel/test_panel.jl") + should_run_test("plotting/test_plotting.jl") && include("plotting/test_plotting.jl") + should_run_test("ram_geometry/test_kite_geometry.jl") && include("ram_geometry/test_kite_geometry.jl") + should_run_test("settings/test_settings.jl") && include("settings/test_settings.jl") + should_run_test("solver/test_solver.jl") && include("solver/test_solver.jl") + should_run_test("solver/test_forwarddiff.jl") && include("solver/test_forwarddiff.jl") + should_run_test("solver/test_backend_comparison.jl") && include("solver/test_backend_comparison.jl") + should_run_test("solver/test_unrefined_dist.jl") && include("solver/test_unrefined_dist.jl") + should_run_test("VortexStepMethod/test_VortexStepMethod.jl") && include("VortexStepMethod/test_VortexStepMethod.jl") + should_run_test("wake/test_wake.jl") && include("wake/test_wake.jl") + should_run_test("wing_geometry/test_wing_geometry.jl") && include("wing_geometry/test_wing_geometry.jl") + should_run_test("wing_geometry/test_billowing.jl") && include("wing_geometry/test_billowing.jl") + should_run_test("yaml_geometry/test_yaml_geometry.jl") && include("yaml_geometry/test_yaml_geometry.jl") + should_run_test("airfoil_aero/test_airfoil_aero.jl") && include("airfoil_aero/test_airfoil_aero.jl") + should_run_test("obj_adapter/test_obj_adapter.jl") && include("obj_adapter/test_obj_adapter.jl") + should_run_test("Aqua.jl") && include("Aqua.jl") end @testset verbose = true "Testing VortexStepMethod..." begin diff --git a/test/test_setup.sh b/test/test_setup.sh index be620253..57e4f274 100755 --- a/test/test_setup.sh +++ b/test/test_setup.sh @@ -58,7 +58,7 @@ fi for f in bench.jl rectangular_wing.jl V3_kite.jl \ pyramid_model.jl ram_air_kite.jl stall_model.jl; do if ! grep -q "using GLMakie" "examples/$f" && \ - ! grep -q "using ControlPlots" "examples/$f"; then + ! grep -q "using MakieControlPlots" "examples/$f"; then pass "$f is backend-agnostic" else fail "$f is backend-agnostic" From 9a7e0e6d81a081402f7123ee57583e65c9dcc147 Mon Sep 17 00:00:00 2001 From: Bart Date: Mon, 13 Jul 2026 12:51:45 +0200 Subject: [PATCH 2/6] CI: run GLMakie/MakieControlPlots under a virtual display MakieControlPlots activates GLMakie on load, whose OpenGL init fails on the headless Linux runners (no DISPLAY). Install xvfb + libgl1-mesa-dri and run the Linux test jobs under `xvfb-run -a`; give the docs job a background Xvfb via DISPLAY. Windows/macOS runners keep their own display context (no prefix). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/CI.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 7933801e..4a09890f 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -45,6 +45,11 @@ jobs: with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} + - name: Install display dependencies for GLMakie + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y xvfb libgl1-mesa-dri - uses: actions/cache@v5 env: cache-name: cache-artifacts @@ -61,6 +66,7 @@ jobs: BUILD_IS_PRODUCTION_BUILD: ${{ matrix.build_is_production_build }} with: coverage: false + prefix: ${{ runner.os == 'Linux' && 'xvfb-run -a' || '' }} test-with-code-coverage: name: Julia ${{ matrix.julia_version }} - ${{ matrix.os }} - ${{ matrix.julia_arch }} - with code coverage timeout-minutes: 30 @@ -81,11 +87,17 @@ jobs: with: arch: ${{ matrix.julia_arch }} version: ${{ matrix.julia_version }} + - name: Install display dependencies for GLMakie + run: | + sudo apt-get update + sudo apt-get install -y xvfb libgl1-mesa-dri - uses: julia-actions/cache@v2 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 env: BUILD_IS_PRODUCTION_BUILD: false + with: + prefix: xvfb-run -a - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v6 with: @@ -101,7 +113,14 @@ jobs: - uses: julia-actions/setup-julia@v3 - uses: julia-actions/cache@v2 - uses: julia-actions/julia-buildpkg@v1 - + + - name: Start virtual display for GLMakie + run: | + sudo apt-get update + sudo apt-get install -y xvfb libgl1-mesa-dri + Xvfb :99 -screen 0 1024x768x24 & + echo "DISPLAY=:99" >> "$GITHUB_ENV" + - uses: julia-actions/julia-docdeploy@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 2f6136a6a13fdd88dbdf8d9c1409e54bdaaa51e5 Mon Sep 17 00:00:00 2001 From: Bart Date: Mon, 13 Jul 2026 12:53:23 +0200 Subject: [PATCH 3/6] CI: give the setup-test example run a display too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test/test_setup.sh runs every example via menu.jl --run-all, which now loads MakieControlPlots (→ GLMakie). Add libgl1-mesa-dri and run the script under xvfb-run so GLMakie can initialize headlessly. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/setup-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/setup-test.yml b/.github/workflows/setup-test.yml index a77fee1c..aba01b9a 100644 --- a/.github/workflows/setup-test.yml +++ b/.github/workflows/setup-test.yml @@ -20,6 +20,6 @@ jobs: version: '1' - uses: julia-actions/cache@v2 - name: Install xvfb for headless GLMakie - run: sudo apt-get update && sudo apt-get install -y xvfb + run: sudo apt-get update && sudo apt-get install -y xvfb libgl1-mesa-dri - name: Run setup tests - run: bash test/test_setup.sh + run: xvfb-run -a bash test/test_setup.sh From 3fdeb0cf7e6ce7d14f92479a1dcd9fa690d6c99a Mon Sep 17 00:00:00 2001 From: Bart Date: Mon, 13 Jul 2026 13:25:09 +0200 Subject: [PATCH 4/6] Examples self-import GLMakie + MakieControlPlots; drop backend-agnostic wiring With ControlPlots gone there is only one plotting backend family, so examples no longer need to stay backend-agnostic for a separate menu_cp() launcher. Every plotting example now imports GLMakie + MakieControlPlots directly (bench/cleanup don't plot, so they don't). Remove the now-meaningless "backend-agnostic" check from test/test_setup.sh and stale two-backend comments in test_plotting.jl. Co-Authored-By: Claude Opus 4.8 (1M context) --- examples/V3_kite.jl | 2 ++ examples/billowing.jl | 2 ++ examples/pyramid_model.jl | 2 ++ examples/rectangular_wing.jl | 2 ++ examples/stall_model.jl | 2 ++ test/plotting/test_plotting.jl | 4 ++-- test/test_setup.sh | 11 +---------- 7 files changed, 13 insertions(+), 12 deletions(-) diff --git a/examples/V3_kite.jl b/examples/V3_kite.jl index 08169f1e..2c09c54b 100644 --- a/examples/V3_kite.jl +++ b/examples/V3_kite.jl @@ -3,6 +3,8 @@ if Base.active_project() != joinpath(@__DIR__, "Project.toml") Pkg.activate(@__DIR__) end using LinearAlgebra +using GLMakie +using MakieControlPlots using VortexStepMethod PLOT = true diff --git a/examples/billowing.jl b/examples/billowing.jl index 1f6b886f..fa42e8c0 100644 --- a/examples/billowing.jl +++ b/examples/billowing.jl @@ -3,6 +3,8 @@ if Base.active_project() != joinpath(@__DIR__, "Project.toml") Pkg.activate(@__DIR__) end using LinearAlgebra +using GLMakie +using MakieControlPlots using VortexStepMethod PLOT = true diff --git a/examples/pyramid_model.jl b/examples/pyramid_model.jl index 9fe18074..445638c0 100644 --- a/examples/pyramid_model.jl +++ b/examples/pyramid_model.jl @@ -3,6 +3,8 @@ if Base.active_project() != joinpath(@__DIR__, "Project.toml") Pkg.activate(@__DIR__) end using LinearAlgebra +using GLMakie +using MakieControlPlots using VortexStepMethod # Load VSM vsm_settings from YAML configuration file diff --git a/examples/rectangular_wing.jl b/examples/rectangular_wing.jl index 1c536573..069db9b1 100644 --- a/examples/rectangular_wing.jl +++ b/examples/rectangular_wing.jl @@ -3,6 +3,8 @@ if Base.active_project() != joinpath(@__DIR__, "Project.toml") Pkg.activate(@__DIR__) end using LinearAlgebra +using GLMakie +using MakieControlPlots using VortexStepMethod PLOT = true diff --git a/examples/stall_model.jl b/examples/stall_model.jl index bcc2c480..2e27641f 100644 --- a/examples/stall_model.jl +++ b/examples/stall_model.jl @@ -3,6 +3,8 @@ if Base.active_project() != joinpath(@__DIR__, "Project.toml") Pkg.activate(@__DIR__) end using LinearAlgebra +using GLMakie +using MakieControlPlots using VortexStepMethod using VortexStepMethod.AirfoilAero: lei_poly_coeffs diff --git a/test/plotting/test_plotting.jl b/test/plotting/test_plotting.jl index 9890db45..54aac4f7 100644 --- a/test/plotting/test_plotting.jl +++ b/test/plotting/test_plotting.jl @@ -200,7 +200,7 @@ end is_show=false) @test fig_rect isa Figure - # Tests for both backends + # Edge cases: empty panels and distributed apparent wind body_aero_empty = create_body_aero() empty!(body_aero_empty.panels) @test_throws Exception plot_geometry( @@ -345,7 +345,7 @@ end ) safe_rm(lit_bad_path) - # Test show_moments=true with literature data (both backends) + # Test show_moments=true with literature data cm_lit_path = tempname() * "_lit_moments.csv" open(cm_lit_path, "w") do io_cm_lit write(io_cm_lit, diff --git a/test/test_setup.sh b/test/test_setup.sh index 57e4f274..ae5a3ca3 100755 --- a/test/test_setup.sh +++ b/test/test_setup.sh @@ -49,21 +49,12 @@ for d in data/ram_air_kite data/TUDELFT_V3_KITE \ [[ -d "$d" ]] && pass "copied $d/" || fail "copied $d/" done -# Verify menu.jl loads GLMakie and examples are backend-agnostic +# Verify menu.jl loads GLMakie if grep -q "using GLMakie" "examples/menu.jl"; then pass "menu.jl uses GLMakie" else fail "menu.jl uses GLMakie" fi -for f in bench.jl rectangular_wing.jl V3_kite.jl \ - pyramid_model.jl ram_air_kite.jl stall_model.jl; do - if ! grep -q "using GLMakie" "examples/$f" && \ - ! grep -q "using MakieControlPlots" "examples/$f"; then - pass "$f is backend-agnostic" - else - fail "$f is backend-agnostic" - fi -done # The copied examples activate their own project (@__DIR__), so ensure that # environment is fully resolved and linked to this checkout before execution. From 3de7b4e2ee05b195d63ece12c8da40b9b36a89d3 Mon Sep 17 00:00:00 2001 From: Bart Date: Mon, 13 Jul 2026 13:43:31 +0200 Subject: [PATCH 5/6] Qualify save() as GLMakie.save() in examples MakieControlPlots exports its own JLD2-based `save`, which clashes with Makie's figure `save` once an example does `using GLMakie` + `using MakieControlPlots`. Qualify the raw figure-save calls (ram_air_kite, V3_neuralfoil) as GLMakie.save. Co-Authored-By: Claude Opus 4.8 (1M context) --- examples/V3_neuralfoil.jl | 2 +- examples/ram_air_kite.jl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/V3_neuralfoil.jl b/examples/V3_neuralfoil.jl index 1f3b2563..df76ac1e 100644 --- a/examples/V3_neuralfoil.jl +++ b/examples/V3_neuralfoil.jl @@ -57,7 +57,7 @@ WRAP = ShrinkWrap(clearance=0.0, min_concave_radius=0.2) # Hover a slice to inspect its 2D airfoil (raw points + the shrink-wrap). fig_slices_3d = plot_slices_3d(OBJ_PATH; n_slices=N_SLICES, rotation=ROTATION, n_bins=N_BINS, wrap_method=WRAP) -save("V3_slices_3d.png", fig_slices_3d) +GLMakie.save("V3_slices_3d.png", fig_slices_3d) # Generate per-slice NeuralFoil polars from the shrink-wrapped sections. XFoil is also # supported (pass `solver=XFoilSolver(...)`) but its panel method does not converge on diff --git a/examples/ram_air_kite.jl b/examples/ram_air_kite.jl index c99f1eaf..81f61dae 100644 --- a/examples/ram_air_kite.jl +++ b/examples/ram_air_kite.jl @@ -58,7 +58,7 @@ WRAP = ShrinkWrap(clearance=0.0) # Hover a slice to inspect its 2D airfoil. fig_slices_3d = plot_slices_3d(obj_path; n_slices=N_SECTIONS, wrap_method=WRAP, wingtip_distance=WINGTIP_DISTANCE, delta=15.0) -save("ram_air_slices_3d.png", fig_slices_3d) +GLMakie.save("ram_air_slices_3d.png", fig_slices_3d) function matrix_wing(solver, subdir; n_panels=50, n_sections=N_SECTIONS) out_dir = joinpath("data", "ram_air_kite", subdir) @@ -79,7 +79,7 @@ wing_xfoil = matrix_wing(XF_SOLVER, "polars_xfoil") # of delta_range; nothing is re-sliced or re-wrapped). fig_audit = plot_slices_3d(joinpath("data", "ram_air_kite", "polars_xfoil"); delta=1.0, obj_path=obj_path) -save("ram_air_slices_audit.png", fig_audit) +GLMakie.save("ram_air_slices_audit.png", fig_audit) body_xfoil = BodyAerodynamics([wing_xfoil]) solver_xfoil = Solver(body_xfoil; aerodynamic_model_type=VSM, rtol=1e-5, solver_type=NONLIN, relaxation_factor=RELAXATION, is_with_artificial_damping=ARTIFICIAL_DAMPING) From c01012401cb3e42e1c494ff449ce655584eeabc7 Mon Sep 17 00:00:00 2001 From: Bart Date: Mon, 13 Jul 2026 14:10:28 +0200 Subject: [PATCH 6/6] Run plotting tests on Linux only GLMakie's PrecompileTools workload creates a screen (glfwInit), which fails on the headless macOS/Windows CI runners (no GPU/display, and xvfb is Linux-only). Loading MakieControlPlots pulls in GLMakie, so gate the plotting test include on Sys.islinux(); the Linux jobs run them under xvfb. All other tests still run on every platform (Windows showed 5111 passing with only the plotting set erroring). Co-Authored-By: Claude Opus 4.8 (1M context) --- test/runtests.jl | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 69ee3153..3409f61b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -40,7 +40,15 @@ function include_selected_tests() should_run_test("filament/test_bound_filament.jl") && include("filament/test_bound_filament.jl") should_run_test("filament/test_semi_infinite_filament.jl") && include("filament/test_semi_infinite_filament.jl") should_run_test("panel/test_panel.jl") && include("panel/test_panel.jl") - should_run_test("plotting/test_plotting.jl") && include("plotting/test_plotting.jl") + if should_run_test("plotting/test_plotting.jl") + # MakieControlPlots activates GLMakie on load, which needs a display. Only the + # Linux CI provides one (via xvfb); skip on headless macOS/Windows runners. + if Sys.islinux() + include("plotting/test_plotting.jl") + else + @info "Skipping plotting tests on $(Sys.KERNEL): GLMakie needs a display." + end + end should_run_test("ram_geometry/test_kite_geometry.jl") && include("ram_geometry/test_kite_geometry.jl") should_run_test("settings/test_settings.jl") && include("settings/test_settings.jl") should_run_test("solver/test_solver.jl") && include("solver/test_solver.jl")