From 9bf3b7b859c63a8d1eac66d1e8515bc4c8d7df15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 2 Jul 2025 13:00:46 +0000 Subject: [PATCH 1/2] Initial plan From 7db5a157c3915ee284526ac2c72dbe287cc5041c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 2 Jul 2025 13:13:23 +0000 Subject: [PATCH 2/2] Implement complete web-based GUI for fuzzy logic experimentation Co-authored-by: amogorkon <16521496+amogorkon@users.noreply.github.com> --- README.md | 41 ++ src/fuzzylogic/__init__.py | 15 +- src/fuzzylogic/gui/README.md | 88 ++++ src/fuzzylogic/gui/__init__.py | 1 + src/fuzzylogic/gui/__main__.py | 9 + src/fuzzylogic/gui/app.py | 757 +++++++++++++++++++++++++++++++++ src/fuzzylogic/gui/cli.py | 47 ++ src/fuzzylogic/gui/example.py | 64 +++ tests/test_gui.py | 110 +++++ 9 files changed, 1131 insertions(+), 1 deletion(-) create mode 100644 src/fuzzylogic/gui/README.md create mode 100644 src/fuzzylogic/gui/__init__.py create mode 100644 src/fuzzylogic/gui/__main__.py create mode 100644 src/fuzzylogic/gui/app.py create mode 100644 src/fuzzylogic/gui/cli.py create mode 100644 src/fuzzylogic/gui/example.py create mode 100644 tests/test_gui.py diff --git a/README.md b/README.md index 132ad6e..2b89d5b 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,47 @@ To make it possible to write fuzzy logic in the most pythonic and simplest way i * Domain and Set uses an assignment trick to make it possible to instantiate Set() without passing domain and name over and over (yet still be explicit, just not the way one would normally expect). This also allows to call sets as Domain.attributes, which also normally shouldn't be possible (since they are technically not attributes). However, this allows interesting things like dangling sets (sets without domains) that can be freely combined with other sets to avoid cluttering of domain-namespaces and just have the resulting set assigned to a domain to work with. # Installation +``` +pip install fuzzylogic +``` + +Note: If you want to use the experimental GUI, you'll also need matplotlib: +``` +pip install matplotlib +``` + +# GUI for Experimentation + +The library now includes a web-based GUI for experimenting with fuzzy logic and generating code! This makes it easy to: + +- Visually create and test fuzzy logic systems +- Experiment with different membership functions +- Generate Python code for your fuzzy logic setup +- Plot and visualize fuzzy sets + +## Starting the GUI + +```python +import fuzzylogic + +# Start the GUI (opens browser automatically) +fuzzylogic.run_gui() + +# Or from command line +# python -m fuzzylogic.gui.cli +``` + + + +## GUI Features + +- **Create Domains**: Define fuzzy logic domains with custom ranges +- **Add Fuzzy Sets**: Create sets using R (rising), S (falling), triangular, trapezoid, and rectangular functions +- **Visualization**: Real-time plotting of fuzzy sets +- **Test Values**: Interactive testing of input values +- **Code Generation**: Automatic Python code generation + +# Documentation Just enter `python -m pip install fuzzylogic` in a commandline prompt and you should be good to go! diff --git a/src/fuzzylogic/__init__.py b/src/fuzzylogic/__init__.py index f7a9134..cdf1c3d 100644 --- a/src/fuzzylogic/__init__.py +++ b/src/fuzzylogic/__init__.py @@ -1 +1,14 @@ -__version__ = (1, 5, 0) +__version__ = (1, 5, 0) + +def run_gui(port=8000): + """Start the fuzzy logic experimentation GUI. + + Args: + port: Port to run the web server on (default: 8000) + """ + try: + from .gui.app import run_gui as _run_gui + _run_gui(port) + except ImportError as e: + print(f"GUI dependencies not available: {e}") + print("Please install matplotlib if you haven't already: pip install matplotlib") diff --git a/src/fuzzylogic/gui/README.md b/src/fuzzylogic/gui/README.md new file mode 100644 index 0000000..14868da --- /dev/null +++ b/src/fuzzylogic/gui/README.md @@ -0,0 +1,88 @@ +# Fuzzy Logic GUI + +This directory contains a web-based GUI for experimenting with fuzzy logic and generating Python code. + +## Features + +- **Create Domains**: Define fuzzy logic domains with custom ranges and resolution +- **Add Fuzzy Sets**: Create fuzzy sets using various membership functions: + - R (Rising): Sigmoid-like function that rises from 0 to 1 + - S (Falling): Sigmoid-like function that falls from 1 to 0 + - Triangular: Triangle-shaped membership function + - Trapezoid: Trapezoid-shaped membership function + - Rectangular: Rectangular/plateau membership function +- **Visualization**: Plot domains and their fuzzy sets using matplotlib +- **Test Values**: Test input values against all sets in a domain +- **Code Generation**: Generate Python code that recreates your fuzzy logic setup + +## Usage + +### Command Line + +Start the GUI from the command line: + +```bash +# From the repository root +python -m fuzzylogic.gui.cli + +# Or with custom port +python -m fuzzylogic.gui.cli --port 8080 + +# Don't open browser automatically +python -m fuzzylogic.gui.cli --no-browser +``` + +### Python API + +Start the GUI programmatically: + +```python +import fuzzylogic + +# Start the GUI (will open browser automatically) +fuzzylogic.run_gui() + +# Or with custom port +fuzzylogic.run_gui(port=8080) +``` + +### Direct Module Usage + +```python +from fuzzylogic.gui.app import run_gui + +# Start the server +run_gui(port=8000) +``` + +## Example Workflow + +1. **Create a Domain**: Enter a name (e.g., "temperature"), set the range (0-40), and click "Create Domain" + +2. **Add Fuzzy Sets**: + - Select the domain from the dropdown + - Enter a set name (e.g., "cold") + - Choose function type (e.g., "S" for falling) + - Set parameters (e.g., low=0, high=15) + - Click "Add Set" + +3. **Visualize**: Select the domain and click "Plot Domain" to see a graph of all fuzzy sets + +4. **Test Values**: Enter a test value and see the membership degrees for each set + +5. **Generate Code**: Click "Generate Python Code" to get Python code that recreates your setup + +## Implementation Details + +The GUI is implemented as a simple HTTP server that serves a single-page web application. It uses: + +- Pure Python with built-in `http.server` (no external web framework dependencies) +- Matplotlib for plotting (with Agg backend for server-side rendering) +- HTML/CSS/JavaScript for the frontend +- JSON API for communication between frontend and backend + +## Files + +- `app.py`: Main GUI application with web server and fuzzy logic interface +- `cli.py`: Command-line interface for starting the GUI +- `__init__.py`: Module initialization \ No newline at end of file diff --git a/src/fuzzylogic/gui/__init__.py b/src/fuzzylogic/gui/__init__.py new file mode 100644 index 0000000..7e99a4b --- /dev/null +++ b/src/fuzzylogic/gui/__init__.py @@ -0,0 +1 @@ +"""GUI module for fuzzy logic experimentation.""" \ No newline at end of file diff --git a/src/fuzzylogic/gui/__main__.py b/src/fuzzylogic/gui/__main__.py new file mode 100644 index 0000000..050f067 --- /dev/null +++ b/src/fuzzylogic/gui/__main__.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +""" +Main entry point for the fuzzy logic GUI command. +""" + +from fuzzylogic.gui.cli import main + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/src/fuzzylogic/gui/app.py b/src/fuzzylogic/gui/app.py new file mode 100644 index 0000000..8e8d5ba --- /dev/null +++ b/src/fuzzylogic/gui/app.py @@ -0,0 +1,757 @@ +#!/usr/bin/env python3 +""" +app.py - Simple web-based GUI for fuzzy logic experimentation and code generation. + +This module provides a web interface for: +- Creating fuzzy logic domains +- Defining fuzzy sets with various membership functions +- Visualizing fuzzy sets +- Testing input values +- Generating Python code +""" + +import json +import os +import base64 +from io import BytesIO +from http.server import HTTPServer, BaseHTTPRequestHandler +from urllib.parse import parse_qs, urlparse +import webbrowser +import threading +import time + +import matplotlib +matplotlib.use('Agg') # Use non-interactive backend +import matplotlib.pyplot as plt + +from fuzzylogic.classes import Domain +from fuzzylogic.functions import R, S, triangular, trapezoid, rectangular + + +class FuzzyLogicGUI: + """Main class for the fuzzy logic GUI.""" + + def __init__(self): + self.domains = {} + self.current_domain = None + + def create_domain(self, name, low, high, resolution=0.1): + """Create a new fuzzy domain.""" + domain = Domain(name, low, high, res=resolution) + self.domains[name] = domain + self.current_domain = domain + return domain + + def add_set_to_domain(self, domain_name, set_name, func_type, params): + """Add a fuzzy set to a domain.""" + if domain_name not in self.domains: + raise ValueError(f"Domain {domain_name} not found") + + domain = self.domains[domain_name] + + # Store function definition for code generation + if not hasattr(self, 'function_definitions'): + self.function_definitions = {} + + self.function_definitions[f"{domain_name}.{set_name}"] = { + 'type': func_type, + 'params': params.copy() + } + + # Create the membership function based on type + if func_type == 'R': + func = R(params['low'], params['high']) + elif func_type == 'S': + func = S(params['low'], params['high']) + elif func_type == 'triangular': + func = triangular(params['low'], params['high'], c=params.get('c')) + elif func_type == 'trapezoid': + func = trapezoid(params['low'], params['c_low'], params['c_high'], params['high']) + elif func_type == 'rectangular': + func = rectangular(params['low'], params['high']) + else: + raise ValueError(f"Unknown function type: {func_type}") + + # Add the set to the domain + setattr(domain, set_name, func) + return domain + + def plot_domain(self, domain_name): + """Create a plot of the domain and return it as base64 encoded image.""" + if domain_name not in self.domains: + return None + + domain = self.domains[domain_name] + + plt.figure(figsize=(10, 6)) + + # Get all fuzzy sets in the domain + fuzzy_sets = [] + for attr_name in dir(domain): + if not attr_name.startswith('_') and attr_name not in ['plot', 'range', 'array']: + attr = getattr(domain, attr_name) + if hasattr(attr, 'func') or hasattr(attr, 'plot'): + fuzzy_sets.append((attr_name, attr)) + + # Plot each set in the domain + if fuzzy_sets: + for set_name, fuzzy_set in fuzzy_sets: + try: + # Create x values for plotting + x_vals = [] + y_vals = [] + for x in domain.range: + x_vals.append(x) + y_vals.append(fuzzy_set(x)) + + plt.plot(x_vals, y_vals, label=f'{domain_name}.{set_name}', linewidth=2) + except Exception as e: + print(f"Warning: Could not plot {set_name}: {e}") + continue + else: + # No sets to plot, show empty domain + plt.plot([domain._low, domain._high], [0, 0], 'k--', alpha=0.3, label='Empty domain') + + plt.title(f'Domain: {domain_name}') + plt.xlabel('Input Value') + plt.ylabel('Membership Degree') + plt.grid(True, alpha=0.3) + plt.ylim(-0.05, 1.05) + plt.xlim(domain._low, domain._high) + + # Only add legend if there are labeled plots + if plt.gca().get_legend_handles_labels()[0]: + plt.legend() + + # Save plot to base64 string + buffer = BytesIO() + plt.savefig(buffer, format='png', dpi=100, bbox_inches='tight') + buffer.seek(0) + plot_data = buffer.getvalue() + buffer.close() + plt.close() + + return base64.b64encode(plot_data).decode() + + def test_value(self, domain_name, value): + """Test a value against all sets in a domain.""" + if domain_name not in self.domains: + return None + + domain = self.domains[domain_name] + result = domain(value) + return {str(k): v for k, v in result.items()} + + def generate_code(self): + """Generate Python code that recreates the current fuzzy logic setup.""" + code_lines = [ + "from fuzzylogic.classes import Domain", + "from fuzzylogic.functions import R, S, triangular, trapezoid, rectangular", + "", + ] + + # Store function definitions for later recreation + function_definitions = getattr(self, 'function_definitions', {}) + + for domain_name, domain in self.domains.items(): + # Create domain + code_lines.append(f"{domain_name} = Domain('{domain_name}', {domain._low}, {domain._high}, res={domain._res})") + + # Add sets using stored function definitions + for func_def_key, func_info in function_definitions.items(): + if func_def_key.startswith(f"{domain_name}."): + set_name = func_def_key.split('.', 1)[1] + + if func_info['type'] == 'R': + code_lines.append(f"{domain_name}.{set_name} = R({func_info['params']['low']}, {func_info['params']['high']})") + elif func_info['type'] == 'S': + code_lines.append(f"{domain_name}.{set_name} = S({func_info['params']['low']}, {func_info['params']['high']})") + elif func_info['type'] == 'triangular': + params = func_info['params'] + if 'c' in params and params['c'] is not None: + code_lines.append(f"{domain_name}.{set_name} = triangular({params['low']}, {params['high']}, c={params['c']})") + else: + code_lines.append(f"{domain_name}.{set_name} = triangular({params['low']}, {params['high']})") + elif func_info['type'] == 'trapezoid': + params = func_info['params'] + code_lines.append(f"{domain_name}.{set_name} = trapezoid({params['low']}, {params['c_low']}, {params['c_high']}, {params['high']})") + elif func_info['type'] == 'rectangular': + params = func_info['params'] + code_lines.append(f"{domain_name}.{set_name} = rectangular({params['low']}, {params['high']})") + + code_lines.append("") + + # Add example usage + if self.domains: + code_lines.extend([ + "# Example usage:", + "# Test a value against all sets in a domain", + ]) + for domain_name in self.domains.keys(): + code_lines.append(f"# result = {domain_name}(value)") + code_lines.append(f"# print(result)") + break # Just show example for one domain + + return "\n".join(code_lines) + + +class FuzzyLogicRequestHandler(BaseHTTPRequestHandler): + """HTTP request handler for the fuzzy logic GUI.""" + + gui_instance = FuzzyLogicGUI() + + def do_GET(self): + """Handle GET requests.""" + parsed_path = urlparse(self.path) + + if parsed_path.path == '/' or parsed_path.path == '/index.html': + self.serve_html() + elif parsed_path.path == '/api/domains': + self.serve_domains() + elif parsed_path.path.startswith('/api/plot/'): + domain_name = parsed_path.path.split('/')[-1] + self.serve_plot(domain_name) + elif parsed_path.path == '/api/code': + self.serve_code() + else: + self.send_error(404) + + def do_POST(self): + """Handle POST requests.""" + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length) + data = json.loads(post_data.decode('utf-8')) + + parsed_path = urlparse(self.path) + + if parsed_path.path == '/api/create_domain': + self.create_domain(data) + elif parsed_path.path == '/api/add_set': + self.add_set(data) + elif parsed_path.path == '/api/test_value': + self.test_value(data) + else: + self.send_error(404) + + def serve_html(self): + """Serve the main HTML interface.""" + html_content = self.get_html_content() + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(html_content.encode()) + + def serve_domains(self): + """Serve the list of domains.""" + domains_info = {} + for name, domain in self.gui_instance.domains.items(): + sets = [] + for attr_name in dir(domain): + if not attr_name.startswith('_') and attr_name not in ['plot', 'range', 'array']: + attr = getattr(domain, attr_name) + if hasattr(attr, 'func'): + sets.append(attr_name) + + domains_info[name] = { + 'low': domain._low, + 'high': domain._high, + 'resolution': domain._res, + 'sets': sets + } + + self.send_json_response(domains_info) + + def serve_plot(self, domain_name): + """Serve a plot for a domain.""" + plot_data = self.gui_instance.plot_domain(domain_name) + if plot_data: + self.send_json_response({'plot': plot_data}) + else: + self.send_error(404) + + def serve_code(self): + """Serve the generated code.""" + code = self.gui_instance.generate_code() + self.send_json_response({'code': code}) + + def create_domain(self, data): + """Create a new domain.""" + try: + domain = self.gui_instance.create_domain( + data['name'], + float(data['low']), + float(data['high']), + float(data.get('resolution', 0.1)) + ) + self.send_json_response({'success': True, 'message': f'Domain {data["name"]} created'}) + except Exception as e: + self.send_json_response({'success': False, 'error': str(e)}) + + def add_set(self, data): + """Add a set to a domain.""" + try: + domain = self.gui_instance.add_set_to_domain( + data['domain_name'], + data['set_name'], + data['func_type'], + data['params'] + ) + self.send_json_response({'success': True, 'message': f'Set {data["set_name"]} added'}) + except Exception as e: + self.send_json_response({'success': False, 'error': str(e)}) + + def test_value(self, data): + """Test a value against a domain.""" + try: + result = self.gui_instance.test_value(data['domain_name'], float(data['value'])) + self.send_json_response({'success': True, 'result': result}) + except Exception as e: + self.send_json_response({'success': False, 'error': str(e)}) + + def send_json_response(self, data): + """Send a JSON response.""" + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps(data).encode()) + + def get_html_content(self): + """Get the HTML content for the interface.""" + return ''' + +
+ + +No plot generated yet
+