Advanced Topics

This section covers advanced usage of MechanicsDSL for expert users and developers looking to extend the framework.

Performance Optimization

Symbolic Simplification

Control simplification to balance speed vs. expression size:

from mechanics_dsl.utils import config

# Increase timeout for complex systems
config.simplification_timeout = 30.0

# Or disable automatic simplification
compiler = PhysicsCompiler()
compiler.symbolic.auto_simplify = False

# Manually simplify critical expressions
simplified = compiler.symbolic.simplify(expr, timeout=60)

Numerical Precision

Adjust solver tolerances for accuracy vs. speed:

# High precision (slow)
solution = compiler.simulate(
    (0, 100),
    rtol=1e-12,
    atol=1e-14,
    method='DOP853'
)

# Lower precision (fast, for exploration)
solution = compiler.simulate(
    (0, 100),
    rtol=1e-6,
    atol=1e-8,
    method='RK45'
)

Caching

Leverage the built-in caching system:

from mechanics_dsl.utils import LRUCache

# Create a cache for expensive computations
jacobian_cache = LRUCache(maxsize=1000)

def cached_jacobian(expr, vars):
    key = (str(expr), tuple(str(v) for v in vars))
    if key in jacobian_cache:
        return jacobian_cache[key]

    result = compute_jacobian(expr, vars)
    jacobian_cache[key] = result
    return result

Stiff Systems

Detecting Stiffness

Some mechanical systems are numerically stiff:

  • Systems with multiple timescales

  • Near equilibrium points

  • High spring constants

# Enable automatic stiffness detection
solution = compiler.simulate(
    (0, 100),
    detect_stiff=True  # Default: True
)

# Check what method was selected
print(f"Method used: {solution['method']}")

Implicit Solvers

For stiff systems, use implicit methods:

# BDF (Backward Differentiation Formula)
solution = compiler.simulate((0, 100), method='BDF')

# Radau IIA (5th order implicit)
solution = compiler.simulate((0, 100), method='Radau')

# LSODA (automatic switching)
solution = compiler.simulate((0, 100), method='LSODA')

Custom Solvers

Implementing Custom Integrators

Extend the solver framework:

from mechanics_dsl.core.solver import NumericalSimulator
import numpy as np

class SymplecticEuler(NumericalSimulator):
    """Symplectic Euler integrator for Hamiltonian systems."""

    def integrate(self, t_span, y0, dt):
        t = np.arange(t_span[0], t_span[1], dt)
        n = len(y0) // 2

        y = np.zeros((len(y0), len(t)))
        y[:, 0] = y0

        for i in range(len(t) - 1):
            q = y[:n, i]
            p = y[n:, i]

            # Symplectic update
            p_new = p + dt * self.dp_dt(q, p)
            q_new = q + dt * self.dq_dt(q, p_new)

            y[:n, i+1] = q_new
            y[n:, i+1] = p_new

        return {'t': t, 'y': y, 'success': True}

Extending the Parser

Adding Custom Commands

Extend the parser for domain-specific commands:

from mechanics_dsl.core.parser import MechanicsParser, ASTNode
from dataclasses import dataclass

@dataclass
class CustomCommandNode(ASTNode):
    name: str
    args: list

class ExtendedParser(MechanicsParser):
    def parse_statement(self):
        token = self.peek()

        if token.value == '\\mycommand':
            return self.parse_mycommand()

        return super().parse_statement()

    def parse_mycommand(self):
        self.expect('COMMAND')  # \mycommand
        self.expect('LBRACE')
        arg = self.parse_expression()
        self.expect('RBRACE')
        return CustomCommandNode('mycommand', [arg])

Adding New Physics Domains

Create a new physics domain by inheriting from PhysicsDomain:

from mechanics_dsl.domains.base import PhysicsDomain
import sympy as sp

class ElectromagneticDomain(PhysicsDomain):
    """Electromagnetic field dynamics."""

    def __init__(self, name='em_system'):
        super().__init__(name)
        self._charge = None
        self._mass = None
        self._E_field = None
        self._B_field = None

    def set_fields(self, E, B):
        self._E_field = E
        self._B_field = B

    def define_lagrangian(self):
        # L = T - V + (q/c) * v · A
        q, m, c = sp.symbols('q m c', real=True)
        # ... implementation
        pass

    def define_hamiltonian(self):
        # H = (p - qA/c)² / 2m + qφ
        pass

    def derive_equations_of_motion(self):
        # Lorentz force: F = q(E + v × B)
        pass

    def get_state_variables(self):
        return ['x', 'y', 'z', 'px', 'py', 'pz']

Code Generation Backends

Creating a New Backend

Add support for a new target platform:

from mechanics_dsl.codegen.base import CodeGenerator

class JuliaGenerator(CodeGenerator):
    """Generate Julia simulation code."""

    @property
    def target_name(self):
        return "julia"

    @property
    def file_extension(self):
        return ".jl"

    def generate(self, output_file):
        code = self._generate_julia_code()
        with open(output_file, 'w') as f:
            f.write(code)
        return output_file

    def generate_equations(self):
        # Convert SymPy to Julia syntax
        from sympy.printing.julia import julia_code

        lines = []
        for coord in self.coordinates:
            expr = self.equations.get(f"{coord}_ddot")
            julia_expr = julia_code(expr)
            lines.append(f"    dydt[{idx}] = {julia_expr}")

        return "\n".join(lines)

Parallel Execution

OpenMP Code Generation

Generate parallelized C++ code:

# Generate OpenMP-parallelized code
compiler.compile_to_cpp(
    "simulation_omp.cpp",
    target="openmp"
)

GPU Acceleration

For large particle systems (SPH, N-body), use GPU backends:

# Generate CUDA code (placeholder)
compiler.compile_to_cpp(
    "simulation.cu",
    target="cuda"
)

Testing and Validation

Analytical Comparisons

Validate against known solutions:

import numpy as np

# Simple pendulum small-angle analytical solution
def analytical_pendulum(t, theta0, l, g):
    omega = np.sqrt(g / l)
    return theta0 * np.cos(omega * t)

# Compare
numerical = solution['y'][0]
analytical = analytical_pendulum(solution['t'], 0.1, 1.0, 9.81)

error = np.max(np.abs(numerical - analytical))
assert error < 1e-6, f"Error too large: {error}"

Energy Conservation

Validate symplectic behavior:

from mechanics_dsl.analysis import EnergyAnalyzer

analyzer = EnergyAnalyzer()
result = analyzer.check_conservation(
    solution, kinetic, potential,
    tolerance=1e-6
)

assert result['conserved'], \
    f"Energy not conserved: {result['max_relative_error']}"

Debugging

Verbose Logging

Enable detailed logs:

from mechanics_dsl.utils import setup_logging

setup_logging(level='DEBUG')

Inspecting Symbolic Expressions

View derived equations:

result = compiler.compile_dsl(source)

# Print equations of motion
for coord, eom in compiler.symbolic.equations.items():
    print(f"{coord} = {eom}")

# Pretty print with SymPy
import sympy as sp
sp.pprint(compiler.symbolic.lagrangian)

Step-by-Step Execution

Debug simulation issues:

# Use small time steps
solution = compiler.simulate(
    (0, 0.1),
    num_points=1000
)

# Check for NaN/Inf
if not np.all(np.isfinite(solution['y'])):
    print("Simulation became unstable!")

    # Find where it failed
    bad_idx = np.where(~np.isfinite(solution['y']))[1][0]
    print(f"Failed at t = {solution['t'][bad_idx]}")