diff --git a/.gitignore b/.gitignore index 6c871484..b5872a75 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dd8fed7..ce0a34a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Project.toml b/Project.toml index 3be70a23..95af8a15 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "VortexStepMethod" uuid = "ed3cd733-9f0f-46a9-93e0-89b8d4998dd9" authors = ["1-Bart-1 ", "Oriol Cayon and contributors"] -version = "3.3.3" +version = "3.3.4" [workspace] projects = ["examples", "examples_cp", "docs", "test"] diff --git a/bin/install_controlplots b/bin/install_controlplots new file mode 100755 index 00000000..ea1ded43 --- /dev/null +++ b/bin/install_controlplots @@ -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 diff --git a/test/Project.toml b/test/Project.toml index 229362d1..8ca3f410 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -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"