Architecture
DifferentialLab follows a modular architecture with clear separation between configuration, numerical solving, plotting, transforms, the GUI, and shared utilities.
Project Layout
src/
├── main_program.py # Entry point, update check on startup
├── pipeline.py # Orchestrates the full solve workflow
├── config/ # Configuration & constants
│ ├── constants.py # App name, version, solver methods, statistics
│ ├── env.py # .env loading, schema, validation
│ ├── paths.py # Output file path management
│ ├── equations.py # Python-callable ODE functions
│ ├── difference_equations.py # Python-callable difference equation functions
│ └── equations.yaml # Predefined ODE/difference/PDE definitions
├── solver/ # Numerical solving engine
│ ├── equation_parser.py # Safe expression parsing (AST-validated)
│ ├── ode_solver.py # solve_ivp wrappers, shooting method
│ ├── difference_solver.py # Iterative solver for recurrence relations
│ ├── pde_solver.py # Finite-difference 2D elliptic PDE solver
│ ├── error_metrics.py # Residual error, Jacobian evals for ODEs
│ ├── predefined.py # YAML equation loader with caching
│ ├── notation.py # FNotation, f-notation to y-notation translation
│ ├── statistics.py # Post-solve statistical analysis (1D and 2D)
│ └── validators.py # Input validation
├── plotting/ # Matplotlib output
│ └── plot_utils.py # Solution plots, phase portraits, surface/contour, save
├── transforms/ # Function transforms
│ ├── function_parser.py # Safe parsing of scalar f(x) expressions
│ └── transform_engine.py # Fourier, Laplace, Taylor, Hilbert, Z-transform
├── complex_problems/ # Special problems with custom UIs (experimental, in development)
│ ├── complex_problems_dialog.py # Problem selection dialog
│ ├── problem_registry.py # Registry of available problems
│ └── coupled_oscillators/ # Coupled harmonic oscillators
│ ├── model.py # Physical model
│ ├── solver.py # Numerical solver
│ ├── ui.py # Problem-specific dialog
│ └── result_dialog.py # Result visualization
├── frontend/ # Tkinter/ttk GUI
│ ├── theme.py # Dark theme, colour helpers
│ ├── window_utils.py # Window centering, modal helpers
│ ├── plot_embed.py # Embed matplotlib in Tk
│ ├── ui_main_menu.py # Main menu window
│ └── ui_dialogs/ # Dialog windows
│ ├── equation_dialog.py
│ ├── parameters_dialog.py
│ ├── result_dialog.py
│ ├── config_dialog.py
│ ├── help_dialog.py
│ ├── transform_dialog.py
│ ├── loading_dialog.py
│ ├── collapsible_section.py
│ ├── scrollable_frame.py
│ ├── keyboard_nav.py
│ └── tooltip.py
└── utils/ # Shared utilities
├── exceptions.py # Custom exception hierarchy
├── expression_parser_shared.py # SAFE_MATH, AST validation for parsers
├── export.py # CSV / JSON / MP4 export
├── logger.py # Logging setup
└── update_checker.py # Weekly version check, git pull
Module Responsibilities
config
Loads and validates all settings from the .env file. ENV_SCHEMA defines
every configurable key with its type, default, and optional allowed values.
Constants (app name, version, solver method list, statistics catalogue) live
in constants.py. paths.py manages timestamped output filenames.
equations.py and difference_equations.py provide Python-callable functions
for complex ODEs and recurrence relations defined in code.
solver
Pure computation with no GUI dependencies. equation_parser safely parses
user-written ODE, vector ODE, PDE, and difference expressions via AST
validation and compiles them into callables. Includes helper _compile_and_test()
for compile-and-validate logic and _load_config_function() to load Python
functions from config modules. ode_solver wraps scipy.integrate.solve_ivp
with a solve_multipoint shooting-method extension and a shared
_resolve_solver_params() helper to eliminate duplicate parameter resolution.
difference_solver iterates recurrence relations. pde_solver solves 2D
elliptic PDEs with finite differences. error_metrics computes residual error
and Jacobian evaluations. notation provides the f-notation layer: FNotation
dataclass, rewrite_f_expression() to translate user f[0]/f[1]/f[i,k]
syntax to internal y[j] arrays for SciPy compatibility, and
generate_derivative_labels() for formatted plot labels (f, f′, f₀, f′₀).
statistics computes post-solve magnitudes (mean, RMS, period, energy, etc.)
for 1D and 2D data, with all_stats linked to AVAILABLE_STATISTICS in
config.constants. validators checks all user inputs before solving and
includes _ordinal() helper for correct ordinal suffix generation and
module-level constants for repeated values.
plotting
Creates matplotlib figures from solution data. plot_utils.py includes
_get_colors() helper for colormap selection with consistent fallback logic.
create_solution_plot renders the solution with configurable line style,
colours, and derivatives. create_phase_plot renders the phase portrait
(f vs f′ or f vs df/dx). create_phase_3d_plot renders 3D parametric
trajectories for vector ODEs with 3+ components. create_surface_plot and
create_contour_plot handle 2D scalar fields. create_vector_animation_plot
and create_vector_animation_3d support vector ODE visualization. All visual
parameters are read from config.env.
transforms
Function transforms for scalar expressions. function_parser parses f(x)
expressions safely and uses vectorized evaluation (passes full NumPy arrays to
eval() for 10–100× speedup on array operations). transform_engine applies
Fourier (FFT), Laplace (real axis), Taylor series, Hilbert (discrete), and
Z-transform. Includes shared helpers _compute_taylor_coeffs() and
_compute_laplace_samples() for identical computation blocks across
apply_transform and get_transform_coefficients. Supports curve view (f vs x)
and coefficients view (a_i vs i). Laplace transform bounds (laplace_s_min,
laplace_s_max) are passed as function parameters to apply_transform and
get_transform_coefficients (defaults 0.1 and 10.0).
complex_problems (experimental)
Special problems with custom UIs, parameters, and visualizations. Still in
development; may contain bugs. The
problem_registry holds ProblemDescriptor entries; each problem provides
an open_dialog(parent) callable. complex_problems_dialog lists available
problems and launches the selected one. The coupled_oscillators submodule
implements a one-dimensional chain of N harmonic oscillators: model.py
defines the physics, solver.py computes the solution, ui.py provides the
parameter dialog, and result_dialog.py shows mode shapes and time evolution.
loading_dialog in frontend.ui_dialogs displays progress during long solves.
frontend
The Tkinter/ttk user interface. theme.py builds a dark theme from env
colours. window_utils.py provides window sizing and centering (center_window,
fit_and_center), modal dialog setup (make_modal), and a reusable
bind_wraplength() helper for dynamic text wrapping across dialogs.
plot_embed.py embeds matplotlib figures in Tkinter with a refactored
_bind_resize_handler() for consistent canvas resize handling.
ui_main_menu.py is the main window with six buttons: Solve, Function
Transform, Complex Problems, Information, Configuration, and Quit. Each
dialog in ui_dialogs/ handles one step of the workflow (equation selection,
parameter input, result display, configuration, help, transforms, loading).
Shared widgets (ScrollableFrame, CollapsibleSection, ToolTip,
keyboard_nav) live alongside the dialogs. The result_dialog.py uses a
shared _save_export_file() handler for CSV and JSON export buttons.
utils
Cross-cutting concerns: a custom exception hierarchy rooted at
DifferentialLabError, expression_parser_shared providing SAFE_MATH,
normalize_unicode_escapes, and validate_expression_ast used by equation
parsers for safe expression evaluation, CSV/JSON/MP4 export helpers with
consistent public API, a logging setup that reads LOG_LEVEL, LOG_FILE, and
LOG_CONSOLE from the environment (now imports get_project_root() from
config.paths to avoid path duplication), and update_checker for weekly
version checks and optional git pull with improved git stash detection logic.
pipeline
The bridge between the GUI and the solver. run_solver_pipeline executes
the full workflow: validate inputs, resolve ODE/difference/PDE function,
solve, compute statistics, and export files. It returns a SolverResult
dataclass containing raw solution data (x, y, statistics, metadata,
notation) — no pre-built matplotlib figures. The result dialog creates plots
interactively from this data, allowing users to select derivatives, phase-space
axes, and visualization modes without re-solving. Detects 1D vs multivariate
from variables and routes to ODE or PDE solver accordingly.
Data Flow
Solve workflow (ODEs, difference equations, PDEs):
User input (GUI)
│
▼
EquationDialog ──► ParametersDialog
│
▼
run_solver_pipeline()
┌─────┼──────────┐
▼ ▼ ▼
validate parse solve_ode /
inputs expr solve_difference /
solve_pde_2d /
solve_multipoint
│
┌──────────┼──────────┐
▼ ▼ ▼
statistics export SolverResult
│ │ (x, y, stats,
└──────────┼ metadata, notation)
▼
ResultDialog (GUI)
creates plots interactively
Complex Problems workflow:
User input (GUI)
│
▼
ComplexProblemsDialog ──► Problem-specific UI (e.g. CoupledOscillatorsDialog)
│
▼
Problem-specific solver
│
▼
Problem-specific result dialog
Import Strategy
Each module’s __init__.py re-exports its public API so external code can
write from solver import solve_ode. Within a module, files import from
siblings directly (from solver.equation_parser import parse_expression) to
avoid circular imports.
Heavy libraries (SciPy, Matplotlib) are imported lazily inside functions to keep startup time low. Dialog classes are also imported lazily when the user clicks a button.
Configuration Lifecycle
At startup,
main_program.pycallsinitialize_and_validate_config(), which loads.envviapython-dotenvand validates every value againstENV_SCHEMA.Invalid values are silently corrected to their defaults with a log warning.
Code reads settings on demand via
get_env_from_schema(key).The Configuration dialog writes a new
.envand restarts the process.
Design Patterns
Module-level constants: Repeated scalar values (e.g., _MAX_FPS, subscripts,
_MAX_GRID_POINTS) are defined at module level rather than inline or inside
functions.
Lazy imports: Heavy dependencies (SciPy, Matplotlib) are imported inside functions to minimize startup time. Configuration-heavy modules use lazy imports to avoid circular dependencies.
Public vs private API: Helper functions prefixed with _ are module-private;
public functions have no underscore prefix. Export public APIs via __init__.py.
Error handling: Expression parsing uses AST validation before compilation to provide clear error messages early. Solver exceptions are caught and displayed to the user via messageboxes rather than crashing.
f-notation vs internal y-notation: Users write equations with f[0], f[1],
f[i,k] (function, derivatives, vector components). The notation module
translates these to internal y[j] arrays required by SciPy’s solve_ivp.
Plot labels and CSV headers use formatted Unicode (f, f′, f₀, f′₀) for clarity.