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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,5 @@ data/ram_air_kite/ram_air_kite_foil_cd_polar.csv
data/ram_air_kite/ram_air_kite_foil_cm_polar.csv
output/
output_cairo/
examples_cp/.CondaPkg/
LocalPreferences.toml
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## VortexStepMethod v3.3.4 2026-05-30

### Added
- `PlotBackend`, `MakieBackend`, `ControlPlotsBackend`, and
`set_plot_backend!` so applications can explicitly choose which plotting
extension the backend-agnostic plotting API should use

### Changed
- backend-agnostic plotting wrappers now route through the active plotting
backend, and each plotting extension initializes itself as the default only
when no backend has been selected yet
- relaxed `ControlPlots` compatibility to include both `0.2.5` and `0.3`

## VortexStepMethod v3.3.3 2026-05-21

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "VortexStepMethod"
uuid = "ed3cd733-9f0f-46a9-93e0-89b8d4998dd9"
authors = ["1-Bart-1 <bart@vandelint.net>", "Oriol Cayon and contributors"]
version = "3.3.3"
version = "3.3.4"

[workspace]
projects = ["examples", "examples_cp", "docs", "test"]
Expand Down
335 changes: 335 additions & 0 deletions bin/install_controlplots
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
#!/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"

# ── 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_prefix="env -u LD_PRELOAD -u LD_LIBRARY_PATH"
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
# 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 both the root project and examples_cp project, because
# `julia --project=examples_cp` reads examples_cp/LocalPreferences.toml,
# not the root one.
_write_system_python_prefs "LocalPreferences.toml"
_write_system_python_prefs "${CONTROLPLOTS_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"

_install_matplotlib_condapkg

_set_mplbackend_qtagg
_verify_controlplots

echo ""
echo "Done. ControlPlots will use the CondaPkg-managed matplotlib with the Qt (qtagg) backend."
fi
2 changes: 1 addition & 1 deletion test/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Aqua = "0.8"
BenchmarkTools = "1"
CSV = "0.10"
CairoMakie = "0"
ControlPlots = "0.2.5"
ControlPlots = "0.2.5, 0.3"
DataFrames = "1.7"
Documenter = "1.8"
Interpolations = "0.15, 0.16"
Expand Down
Loading