spagestic commited on
Commit
3dd583e
·
1 Parent(s): 544c80a

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
- def parse_float_list(input_str: str, expected_len: int = 0) -> List[float]:
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
- fn=lambda ode_str, t_span_str, y0_str, t_eval_count, method: solve_first_order_ode(
95
- string_to_ode_func(ode_str, ('t', 'y')),
96
- parse_time_span(t_span_str),
97
- parse_float_list(y0_str), # y0 can be a list for systems
98
- int(t_eval_count),
99
- method
100
- ),
101
- inputs=[
102
- gr.Textbox(label="ODE Function (lambda t, y: ...)",
103
- placeholder="e.g., lambda t, y: -y*t OR for system lambda t, y: [y[1], -0.1*y[1] - y[0]]",
104
- info="Define dy/dt or a system [dy1/dt, dy2/dt,...]. `y` is a list/array for systems."),
105
- gr.Textbox(label="Time Span (t_start, t_end)", placeholder="e.g., 0,10"),
106
- gr.Textbox(label="Initial Condition(s) y(t_start)", placeholder="e.g., 1 OR for system 1,0"),
107
- gr.Slider(minimum=10, maximum=1000, value=100, step=10, label="Evaluation Points Count"),
108
- gr.Radio(choices=['RK45', 'LSODA', 'BDF', 'RK23', 'DOP853'], value='RK45', label="Solver Method")
109
- ],
110
- outputs=[
111
- gr.Image(label="Solution Plot", type="filepath", show_label=True, visible=lambda res: res['success'] and res['plot_path'] is not None),
112
- gr.Textbox(label="Solver Message"),
113
- gr.Textbox(label="Success Status"),
114
- gr.JSON(label="Raw Data (t, y values)", visible=lambda res: res['success']) # For users to copy if needed
115
- ],
116
- title="First-Order ODE Solver",
117
- description="Solves dy/dt = f(t,y) or a system of first-order ODEs. " \
118
- "WARNING: Uses eval() for the ODE function string - potential security risk. " \
119
- "For systems, `y` in lambda is `[y1, y2, ...]`, return `[dy1/dt, dy2/dt, ...]`. " \
120
- "Example (Damped Oscillator): ODE: lambda t, y: [y[1], -0.5*y[1] - y[0]], y0: 1,0, Timespan: 0,20",
121
- flagging_mode="manual"
122
- )
123
-
124
- # --- Gradio Interface for Second-Order ODEs ---
125
- second_order_ode_interface = gr.Interface(
126
- fn=lambda ode_str, t_span_str, y0_val_str, dy0_dt_val_str, t_eval_count, method: solve_second_order_ode(
127
- string_to_ode_func(ode_str, ('t', 'y', 'dy_dt')), # Note: dy_dt is one variable name here
128
- parse_time_span(t_span_str),
129
- parse_float_list(y0_val_str, expected_len=1)[0], # y0 is a single float
130
- parse_float_list(dy0_dt_val_str, expected_len=1)[0], # dy0_dt is a single float
131
- int(t_eval_count),
132
- method
133
- ),
134
- inputs=[
135
- gr.Textbox(label="ODE Function (lambda t, y, dy_dt: ...)",
136
- placeholder="e.g., lambda t, y, dy_dt: -0.1*dy_dt - math.sin(y)",
137
- info="Define d²y/dt². `y` is current value, `dy_dt` is current first derivative."),
138
- gr.Textbox(label="Time Span (t_start, t_end)", placeholder="e.g., 0,20"),
139
- gr.Textbox(label="Initial y(t_start)", placeholder="e.g., 1.0"),
140
- gr.Textbox(label="Initial dy/dt(t_start)", placeholder="e.g., 0.0"),
141
- gr.Slider(minimum=10, maximum=1000, value=100, step=10, label="Evaluation Points Count"),
142
- gr.Radio(choices=['RK45', 'LSODA', 'BDF', 'RK23', 'DOP853'], value='RK45', label="Solver Method")
143
- ],
144
- outputs=[
145
- 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),
146
- gr.Textbox(label="Solver Message"),
147
- gr.Textbox(label="Success Status"),
148
- gr.JSON(label="Raw Data (t, y, dy_dt values)", visible=lambda res: res['success'])
149
- ],
150
- title="Second-Order ODE Solver",
151
- description="Solves d²y/dt² = f(t, y, dy/dt). " \
152
- "WARNING: Uses eval() for the ODE function string - potential security risk. " \
153
- "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",
154
- flagging_mode="manual"
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.differential_equations_interface import (
3
- first_order_ode_interface, second_order_ode_interface
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
+ )