Spaces:
Runtime error
Runtime error
feat: modularize differential equations interfaces and add utility functions for parsing and evaluation
Browse files
maths/differential_equations/differential_equations_interface.py
CHANGED
|
@@ -16,143 +16,76 @@ import math # To make math functions available in eval scope for ODEs
|
|
| 16 |
# Import the solver functions
|
| 17 |
from maths.differential_equations.solve_first_order_ode import solve_first_order_ode
|
| 18 |
from maths.differential_equations.solve_second_order_ode import solve_second_order_ode
|
|
|
|
| 19 |
|
| 20 |
# --- Helper Functions ---
|
| 21 |
|
| 22 |
-
|
| 23 |
-
"""Parses a comma-separated string of floats into a list."""
|
| 24 |
-
try:
|
| 25 |
-
if not input_str.strip():
|
| 26 |
-
if expected_len > 0: # If expecting specific number, empty is error
|
| 27 |
-
raise ValueError("Input string is empty.")
|
| 28 |
-
return [] # Allow empty list if not expecting specific length
|
| 29 |
-
|
| 30 |
-
parts = [float(p.strip()) for p in input_str.split(',') if p.strip()]
|
| 31 |
-
if expected_len > 0 and len(parts) != expected_len:
|
| 32 |
-
raise ValueError(f"Expected {expected_len} values, but got {len(parts)}.")
|
| 33 |
-
return parts
|
| 34 |
-
except ValueError as e:
|
| 35 |
-
raise gr.Error(f"Invalid format for list of numbers. Use comma-separated floats. Error: {e}")
|
| 36 |
-
|
| 37 |
-
def parse_time_span(time_span_str: str) -> Tuple[float, float]:
|
| 38 |
-
"""Parses a comma-separated string for time span (t_start, t_end)."""
|
| 39 |
-
parts = parse_float_list(time_span_str, expected_len=2)
|
| 40 |
-
if parts[0] >= parts[1]:
|
| 41 |
-
raise gr.Error("t_start must be less than t_end for the time span.")
|
| 42 |
-
return (parts[0], parts[1])
|
| 43 |
-
|
| 44 |
-
def string_to_ode_func(lambda_str: str, expected_args: Tuple[str, ...]) -> Callable:
|
| 45 |
-
"""
|
| 46 |
-
Converts a string representation of a Python lambda function into a callable.
|
| 47 |
-
Includes a basic check for 'lambda' keyword and argument count.
|
| 48 |
-
|
| 49 |
-
Args:
|
| 50 |
-
lambda_str: The string, e.g., "lambda t, y: -y" or "lambda t, y, dy_dt: -0.1*dy_dt -y".
|
| 51 |
-
expected_args: A tuple of expected argument names, e.g., ('t', 'y') or ('t', 'y', 'dy_dt').
|
| 52 |
-
|
| 53 |
-
Returns:
|
| 54 |
-
A callable function.
|
| 55 |
-
Raises:
|
| 56 |
-
gr.Error: If the string is not a valid lambda or has wrong argument structure.
|
| 57 |
-
"""
|
| 58 |
-
lambda_str = lambda_str.strip()
|
| 59 |
-
if not lambda_str.startswith("lambda"):
|
| 60 |
-
raise gr.Error("ODE function must be a Python lambda string (e.g., 'lambda t, y: -y').")
|
| 61 |
-
|
| 62 |
-
try:
|
| 63 |
-
# Basic check for argument names within the lambda definition part (before ':')
|
| 64 |
-
# This is a heuristic and not a full AST parse for safety here, as eval is the main concern.
|
| 65 |
-
lambda_def_part = lambda_str.split(":")[0]
|
| 66 |
-
for arg_name in expected_args:
|
| 67 |
-
if arg_name not in lambda_def_part:
|
| 68 |
-
raise gr.Error(f"Lambda function string does not seem to contain expected argument: '{arg_name}'. Expected args: {expected_args}")
|
| 69 |
-
|
| 70 |
-
# The eval environment will have access to common math functions and numpy (np)
|
| 71 |
-
# This is where the security risk lies.
|
| 72 |
-
safe_eval_globals = {
|
| 73 |
-
"np": np,
|
| 74 |
-
"math": math,
|
| 75 |
-
"sin": math.sin, "cos": math.cos, "tan": math.tan,
|
| 76 |
-
"exp": math.exp, "log": math.log, "log10": math.log10,
|
| 77 |
-
"sqrt": math.sqrt, "fabs": math.fabs, "pow": math.pow,
|
| 78 |
-
"pi": math.pi, "e": math.e
|
| 79 |
-
}
|
| 80 |
-
# User must use 'math.func' or 'np.func' for most things not listed above.
|
| 81 |
-
|
| 82 |
-
func = eval(lambda_str, safe_eval_globals, {})
|
| 83 |
-
if not callable(func):
|
| 84 |
-
raise gr.Error("The provided string did not evaluate to a callable function.")
|
| 85 |
-
return func
|
| 86 |
-
except SyntaxError as se:
|
| 87 |
-
raise gr.Error(f"Syntax error in lambda function: {se}. Ensure it's valid Python syntax.")
|
| 88 |
-
except Exception as e:
|
| 89 |
-
raise gr.Error(f"Error evaluating lambda function string: {e}. Ensure it's a valid lambda returning the derivative(s).")
|
| 90 |
-
|
| 91 |
|
| 92 |
# --- Gradio Interface for First-Order ODEs ---
|
| 93 |
-
first_order_ode_interface = gr.Interface(
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
)
|
| 123 |
-
|
| 124 |
-
# --- Gradio Interface for Second-Order ODEs ---
|
| 125 |
-
second_order_ode_interface = gr.Interface(
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
)
|
| 156 |
|
| 157 |
# Example usage for testing (can be removed)
|
| 158 |
if __name__ == '__main__':
|
|
|
|
| 16 |
# Import the solver functions
|
| 17 |
from maths.differential_equations.solve_first_order_ode import solve_first_order_ode
|
| 18 |
from maths.differential_equations.solve_second_order_ode import solve_second_order_ode
|
| 19 |
+
from maths.differential_equations.ode_interface_utils import parse_float_list, parse_time_span, string_to_ode_func
|
| 20 |
|
| 21 |
# --- Helper Functions ---
|
| 22 |
|
| 23 |
+
# (Keep the helper functions here if needed, otherwise they can be removed as they are now in ode_interface_utils.py)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
# --- Gradio Interface for First-Order ODEs ---
|
| 26 |
+
# first_order_ode_interface = gr.Interface(
|
| 27 |
+
# fn=lambda ode_str, t_span_str, y0_str, t_eval_count, method: solve_first_order_ode(
|
| 28 |
+
# string_to_ode_func(ode_str, ('t', 'y')),
|
| 29 |
+
# parse_time_span(t_span_str),
|
| 30 |
+
# parse_float_list(y0_str), # y0 can be a list for systems
|
| 31 |
+
# int(t_eval_count),
|
| 32 |
+
# method
|
| 33 |
+
# ),
|
| 34 |
+
# inputs=[
|
| 35 |
+
# gr.Textbox(label="ODE Function (lambda t, y: ...)",
|
| 36 |
+
# placeholder="e.g., lambda t, y: -y*t OR for system lambda t, y: [y[1], -0.1*y[1] - y[0]]",
|
| 37 |
+
# info="Define dy/dt or a system [dy1/dt, dy2/dt,...]. `y` is a list/array for systems."),
|
| 38 |
+
# gr.Textbox(label="Time Span (t_start, t_end)", placeholder="e.g., 0,10"),
|
| 39 |
+
# gr.Textbox(label="Initial Condition(s) y(t_start)", placeholder="e.g., 1 OR for system 1,0"),
|
| 40 |
+
# gr.Slider(minimum=10, maximum=1000, value=100, step=10, label="Evaluation Points Count"),
|
| 41 |
+
# gr.Radio(choices=['RK45', 'LSODA', 'BDF', 'RK23', 'DOP853'], value='RK45', label="Solver Method")
|
| 42 |
+
# ],
|
| 43 |
+
# outputs=[
|
| 44 |
+
# gr.Image(label="Solution Plot", type="filepath", show_label=True, visible=lambda res: res['success'] and res['plot_path'] is not None),
|
| 45 |
+
# gr.Textbox(label="Solver Message"),
|
| 46 |
+
# gr.Textbox(label="Success Status"),
|
| 47 |
+
# gr.JSON(label="Raw Data (t, y values)", visible=lambda res: res['success']) # For users to copy if needed
|
| 48 |
+
# ],
|
| 49 |
+
# title="First-Order ODE Solver",
|
| 50 |
+
# description="Solves dy/dt = f(t,y) or a system of first-order ODEs. " \
|
| 51 |
+
# "WARNING: Uses eval() for the ODE function string - potential security risk. " \
|
| 52 |
+
# "For systems, `y` in lambda is `[y1, y2, ...]`, return `[dy1/dt, dy2/dt, ...]`. " \
|
| 53 |
+
# "Example (Damped Oscillator): ODE: lambda t, y: [y[1], -0.5*y[1] - y[0]], y0: 1,0, Timespan: 0,20",
|
| 54 |
+
# flagging_mode="manual"
|
| 55 |
+
# )
|
| 56 |
+
|
| 57 |
+
# # --- Gradio Interface for Second-Order ODEs ---
|
| 58 |
+
# second_order_ode_interface = gr.Interface(
|
| 59 |
+
# fn=lambda ode_str, t_span_str, y0_val_str, dy0_dt_val_str, t_eval_count, method: solve_second_order_ode(
|
| 60 |
+
# string_to_ode_func(ode_str, ('t', 'y', 'dy_dt')), # Note: dy_dt is one variable name here
|
| 61 |
+
# parse_time_span(t_span_str),
|
| 62 |
+
# parse_float_list(y0_val_str, expected_len=1)[0], # y0 is a single float
|
| 63 |
+
# parse_float_list(dy0_dt_val_str, expected_len=1)[0], # dy0_dt is a single float
|
| 64 |
+
# int(t_eval_count),
|
| 65 |
+
# method
|
| 66 |
+
# ),
|
| 67 |
+
# inputs=[
|
| 68 |
+
# gr.Textbox(label="ODE Function (lambda t, y, dy_dt: ...)",
|
| 69 |
+
# placeholder="e.g., lambda t, y, dy_dt: -0.1*dy_dt - math.sin(y)",
|
| 70 |
+
# info="Define d²y/dt². `y` is current value, `dy_dt` is current first derivative."),
|
| 71 |
+
# gr.Textbox(label="Time Span (t_start, t_end)", placeholder="e.g., 0,20"),
|
| 72 |
+
# gr.Textbox(label="Initial y(t_start)", placeholder="e.g., 1.0"),
|
| 73 |
+
# gr.Textbox(label="Initial dy/dt(t_start)", placeholder="e.g., 0.0"),
|
| 74 |
+
# gr.Slider(minimum=10, maximum=1000, value=100, step=10, label="Evaluation Points Count"),
|
| 75 |
+
# gr.Radio(choices=['RK45', 'LSODA', 'BDF', 'RK23', 'DOP853'], value='RK45', label="Solver Method")
|
| 76 |
+
# ],
|
| 77 |
+
# outputs=[
|
| 78 |
+
# gr.Image(label="Solution Plot (y(t) and dy/dt(t))", type="filepath", show_label=True, visible=lambda res: res['success'] and res['plot_path'] is not None),
|
| 79 |
+
# gr.Textbox(label="Solver Message"),
|
| 80 |
+
# gr.Textbox(label="Success Status"),
|
| 81 |
+
# gr.JSON(label="Raw Data (t, y, dy_dt values)", visible=lambda res: res['success'])
|
| 82 |
+
# ],
|
| 83 |
+
# title="Second-Order ODE Solver",
|
| 84 |
+
# description="Solves d²y/dt² = f(t, y, dy/dt). " \
|
| 85 |
+
# "WARNING: Uses eval() for the ODE function string - potential security risk. " \
|
| 86 |
+
# "Example (Pendulum): ODE: lambda t, y, dy_dt: -9.81/1.0 * math.sin(y), y0: math.pi/4, dy0/dt: 0, Timespan: 0,10",
|
| 87 |
+
# flagging_mode="manual"
|
| 88 |
+
# )
|
| 89 |
|
| 90 |
# Example usage for testing (can be removed)
|
| 91 |
if __name__ == '__main__':
|
maths/differential_equations/differential_equations_tab.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
-
from maths.differential_equations.
|
| 3 |
-
|
| 4 |
-
)
|
| 5 |
|
| 6 |
differential_equations_interfaces_list = [first_order_ode_interface, second_order_ode_interface]
|
| 7 |
differential_equations_tab_names = ["First Order ODE", "Second Order ODE"]
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
+
from maths.differential_equations.solve_first_order_ode import first_order_ode_interface
|
| 3 |
+
from maths.differential_equations.solve_second_order_ode import second_order_ode_interface
|
|
|
|
| 4 |
|
| 5 |
differential_equations_interfaces_list = [first_order_ode_interface, second_order_ode_interface]
|
| 6 |
differential_equations_tab_names = ["First Order ODE", "Second Order ODE"]
|
maths/differential_equations/ode_interface_utils.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Helper functions for parsing and ODE lambda string evaluation for ODE solvers.
|
| 3 |
+
"""
|
| 4 |
+
import gradio as gr
|
| 5 |
+
import numpy as np
|
| 6 |
+
from typing import List, Tuple, Callable
|
| 7 |
+
import math
|
| 8 |
+
|
| 9 |
+
def parse_float_list(input_str: str, expected_len: int = 0) -> List[float]:
|
| 10 |
+
try:
|
| 11 |
+
if not input_str.strip():
|
| 12 |
+
if expected_len > 0:
|
| 13 |
+
raise ValueError("Input string is empty.")
|
| 14 |
+
return []
|
| 15 |
+
parts = [float(p.strip()) for p in input_str.split(',') if p.strip()]
|
| 16 |
+
if expected_len > 0 and len(parts) != expected_len:
|
| 17 |
+
raise ValueError(f"Expected {expected_len} values, but got {len(parts)}.")
|
| 18 |
+
return parts
|
| 19 |
+
except ValueError as e:
|
| 20 |
+
raise gr.Error(f"Invalid format for list of numbers. Use comma-separated floats. Error: {e}")
|
| 21 |
+
|
| 22 |
+
def parse_time_span(time_span_str: str) -> Tuple[float, float]:
|
| 23 |
+
parts = parse_float_list(time_span_str, expected_len=2)
|
| 24 |
+
if parts[0] >= parts[1]:
|
| 25 |
+
raise gr.Error("t_start must be less than t_end for the time span.")
|
| 26 |
+
return (parts[0], parts[1])
|
| 27 |
+
|
| 28 |
+
def string_to_ode_func(lambda_str: str, expected_args: Tuple[str, ...]) -> Callable:
|
| 29 |
+
lambda_str = lambda_str.strip()
|
| 30 |
+
if not lambda_str.startswith("lambda"):
|
| 31 |
+
raise gr.Error("ODE function must be a Python lambda string (e.g., 'lambda t, y: -y').")
|
| 32 |
+
try:
|
| 33 |
+
lambda_def_part = lambda_str.split(":")[0]
|
| 34 |
+
for arg_name in expected_args:
|
| 35 |
+
if arg_name not in lambda_def_part:
|
| 36 |
+
raise gr.Error(f"Lambda function string does not seem to contain expected argument: '{arg_name}'. Expected args: {expected_args}")
|
| 37 |
+
safe_eval_globals = {
|
| 38 |
+
"np": np,
|
| 39 |
+
"math": math,
|
| 40 |
+
"sin": math.sin, "cos": math.cos, "tan": math.tan,
|
| 41 |
+
"exp": math.exp, "log": math.log, "log10": math.log10,
|
| 42 |
+
"sqrt": math.sqrt, "fabs": math.fabs, "pow": math.pow,
|
| 43 |
+
"pi": math.pi, "e": math.e
|
| 44 |
+
}
|
| 45 |
+
func = eval(lambda_str, safe_eval_globals, {})
|
| 46 |
+
if not callable(func):
|
| 47 |
+
raise gr.Error("The provided string did not evaluate to a callable function.")
|
| 48 |
+
return func
|
| 49 |
+
except SyntaxError as se:
|
| 50 |
+
raise gr.Error(f"Syntax error in lambda function: {se}. Ensure it's valid Python syntax.")
|
| 51 |
+
except Exception as e:
|
| 52 |
+
raise gr.Error(f"Error evaluating lambda function string: {e}. Ensure it's a valid lambda returning the derivative(s).")
|
maths/differential_equations/solve_second_order_ode.py
CHANGED
|
@@ -6,6 +6,8 @@ import numpy as np
|
|
| 6 |
from scipy.integrate import solve_ivp
|
| 7 |
from typing import Callable, List, Tuple, Dict, Any, Union
|
| 8 |
import matplotlib.pyplot as plt
|
|
|
|
|
|
|
| 9 |
|
| 10 |
def solve_second_order_ode(
|
| 11 |
ode_func_second_order: Callable[[float, float, float], float],
|
|
@@ -74,3 +76,36 @@ def solve_second_order_ode(
|
|
| 74 |
'success': False,
|
| 75 |
'plot_path': None
|
| 76 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
from scipy.integrate import solve_ivp
|
| 7 |
from typing import Callable, List, Tuple, Dict, Any, Union
|
| 8 |
import matplotlib.pyplot as plt
|
| 9 |
+
from maths.differential_equations.ode_interface_utils import parse_float_list, parse_time_span, string_to_ode_func
|
| 10 |
+
import gradio as gr
|
| 11 |
|
| 12 |
def solve_second_order_ode(
|
| 13 |
ode_func_second_order: Callable[[float, float, float], float],
|
|
|
|
| 76 |
'success': False,
|
| 77 |
'plot_path': None
|
| 78 |
}
|
| 79 |
+
|
| 80 |
+
# --- Gradio Interface for Second-Order ODEs ---
|
| 81 |
+
second_order_ode_interface = gr.Interface(
|
| 82 |
+
fn=lambda ode_str, t_span_str, y0_val_str, dy0_dt_val_str, t_eval_count, method: solve_second_order_ode(
|
| 83 |
+
string_to_ode_func(ode_str, ('t', 'y', 'dy_dt')),
|
| 84 |
+
parse_time_span(t_span_str),
|
| 85 |
+
parse_float_list(y0_val_str, expected_len=1)[0],
|
| 86 |
+
parse_float_list(dy0_dt_val_str, expected_len=1)[0],
|
| 87 |
+
int(t_eval_count),
|
| 88 |
+
method
|
| 89 |
+
),
|
| 90 |
+
inputs=[
|
| 91 |
+
gr.Textbox(label="ODE Function (lambda t, y, dy_dt: ...)",
|
| 92 |
+
placeholder="e.g., lambda t, y, dy_dt: -0.1*dy_dt - math.sin(y)",
|
| 93 |
+
info="Define d²y/dt². `y` is current value, `dy_dt` is current first derivative."),
|
| 94 |
+
gr.Textbox(label="Time Span (t_start, t_end)", placeholder="e.g., 0,20"),
|
| 95 |
+
gr.Textbox(label="Initial y(t_start)", placeholder="e.g., 1.0"),
|
| 96 |
+
gr.Textbox(label="Initial dy/dt(t_start)", placeholder="e.g., 0.0"),
|
| 97 |
+
gr.Slider(minimum=10, maximum=1000, value=100, step=10, label="Evaluation Points Count"),
|
| 98 |
+
gr.Radio(choices=['RK45', 'LSODA', 'BDF', 'RK23', 'DOP853'], value='RK45', label="Solver Method")
|
| 99 |
+
],
|
| 100 |
+
outputs=[
|
| 101 |
+
gr.Image(label="Solution Plot (y(t) and dy/dt(t))", type="filepath", show_label=True, visible=lambda res: res['success'] and res['plot_path'] is not None),
|
| 102 |
+
gr.Textbox(label="Solver Message"),
|
| 103 |
+
gr.Textbox(label="Success Status"),
|
| 104 |
+
gr.JSON(label="Raw Data (t, y, dy_dt values)", visible=lambda res: res['success'])
|
| 105 |
+
],
|
| 106 |
+
title="Second-Order ODE Solver",
|
| 107 |
+
description="Solves d²y/dt² = f(t, y, dy/dt). "
|
| 108 |
+
"WARNING: Uses eval() for the ODE function string - potential security risk. "
|
| 109 |
+
"Example (Pendulum): ODE: lambda t, y, dy_dt: -9.81/1.0 * math.sin(y), y0: math.pi/4, dy0/dt: 0, Timespan: 0,10",
|
| 110 |
+
flagging_mode="manual"
|
| 111 |
+
)
|