From 72282285f84a46b6c6d9ade1c984b8c04f58546a Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 17 Apr 2026 19:50:22 +0200 Subject: add mathematical plotting Signed-off-by: salmonik --- .gitignore | 3 +- cogs/plot.py | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 8 ++++ 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 cogs/plot.py diff --git a/.gitignore b/.gitignore index 72bed63..511ed04 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ cogs/__pycache__ cogs/music/__pycache__ data/ .idea/ -.venv/ \ No newline at end of file +.venv/ +.zed diff --git a/cogs/plot.py b/cogs/plot.py new file mode 100644 index 0000000..1516bc1 --- /dev/null +++ b/cogs/plot.py @@ -0,0 +1,128 @@ +# This file is part of Senator Sperma +# Licensed under the GPL-3.0 License +# Copyright (C) 2026 Salmonik @ salmonik@tuta.io +# https://codeberg.org/salmonik/senator-sperma + +import asyncio +import logging +import math +import os +import re + +import discord +from discord.ext import commands + +logger = logging.getLogger(__name__) + +try: + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + PLOT_AVAILABLE = True +except ImportError: + PLOT_AVAILABLE = False + logger.warning("matplotlib not available, plot command will be disabled") + + +def parse_expression(expr: str) -> str: + """Convert user expression (x^3) to Python (x**3)""" + expr = expr.replace("^", "**") + expr = re.sub(r"(\d)([x])", r"\1*\2", expr) + expr = re.sub(r"([x])(\d)", r"\1*\2", expr) + return expr + + +class Plot(commands.Cog): + def __init__(self, bot): + self.bot = bot + + def _generate_plot(self, func_expr: str, output_path: str): + """Generate plot (runs in executor)""" + parsed_expr = parse_expression(func_expr) + + x = [i * 0.1 for i in range(-100, 101)] + y = [] + + for xi in x: + try: + result = eval( + parsed_expr, + { + "x": xi, + "math": math, + "sin": math.sin, + "cos": math.cos, + "tan": math.tan, + "sqrt": math.sqrt, + "log": math.log, + "exp": math.exp, + "pi": math.pi, + "e": math.e, + }, + ) + if math.isfinite(result): + y.append(result) + else: + y.append(float("nan")) + except Exception: + y.append(float("nan")) + + fig, ax = plt.subplots(figsize=(10, 6)) + ax.plot(x, y, linewidth=2, color="#00ff00") + ax.set_title(f"y = {func_expr}", fontsize=14) + ax.set_xlabel("x", fontsize=12) + ax.set_ylabel("y", fontsize=12) + ax.grid(True, alpha=0.3) + ax.axhline(y=0, color="white", linewidth=0.5) + ax.axvline(x=0, color="white", linewidth=0.5) + ax.set_facecolor("#1a1a1a") + fig.patch.set_facecolor("#1a1a1a") + + plt.savefig(output_path, dpi=100, bbox_inches="tight") + plt.close() + + @commands.slash_command( + name="funkcja", description="Generuje wykres funkcji matematycznej" + ) + async def plot( + self, + ctx, + funkcja: str = discord.Option( + str, "Funkcja np. x^3, sin(x), x^2 + 2*x - 1", required=True + ), + ): + if not PLOT_AVAILABLE: + await ctx.respond( + "Generowanie wykresów nie jest dostępne (brak matplotlib)", + ephemeral=True, + ) + return + + await ctx.defer() + + temp_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "temp") + os.makedirs(temp_dir, exist_ok=True) + temp_path = os.path.join( + temp_dir, f"plot_{os.getpid()}_{asyncio.get_event_loop().time()}.png" + ) + + try: + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, self._generate_plot, funkcja, temp_path) + await ctx.respond(file=discord.File(temp_path, filename="plot.png")) + logger.info(f"Generated plot for function: {funkcja}") + except Exception as e: + logger.error(f"Plot generation error: {e}") + await ctx.respond(f"Błąd generowania wykresu: {str(e)}", ephemeral=True) + finally: + try: + if os.path.exists(temp_path): + os.unlink(temp_path) + except Exception as e: + logger.error(f"Error cleaning up temp file: {e}") + + +def setup(bot): + bot.add_cog(Plot(bot)) diff --git a/requirements.txt b/requirements.txt index e5dc093..863badd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,22 +10,30 @@ certifi==2026.2.25 cffi==2.0.0 charset-normalizer==3.4.6 click==8.1.8 +contourpy==1.3.3 croniter==6.2.2 +cycler==0.12.1 davey==0.1.4 dotenv==0.9.9 +fonttools==4.62.1 frozenlist==1.8.0 gTTS==2.5.4 h11==0.16.0 httpcore==1.0.9 httpx==0.28.1 idna==3.11 +kiwisolver==1.5.0 +matplotlib==3.10.8 multidict==6.7.1 mutagen==1.47.0 +numpy==2.4.4 +packaging==26.1 pillow==12.2.0 propcache==0.4.1 py-cord @ file:///home/salmon/Documents/botcode/senator-sperma/pycord pycparser==3.0 PyNaCl==1.6.2 +pyparsing==3.3.2 python-dateutil==2.9.0.post0 python-dotenv==1.2.2 pytube==15.0.0 -- cgit v1.2.3