+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 382 of 541

๐Ÿ“˜ SciPy: Scientific Computing

Master scipy: scientific computing in Python with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

Prerequisites

  • Basic understanding of programming concepts ๐Ÿ“
  • Python installation (3.8+) ๐Ÿ
  • VS Code or preferred IDE ๐Ÿ’ป

What you'll learn

  • Understand the concept fundamentals ๐ŸŽฏ
  • Apply the concept in real projects ๐Ÿ—๏ธ
  • Debug common issues ๐Ÿ›
  • Write clean, Pythonic code โœจ

๐ŸŽฏ Introduction

Welcome to the fascinating world of scientific computing with SciPy! ๐ŸŽ‰ If youโ€™ve ever wondered how scientists and engineers solve complex mathematical problems with Python, youโ€™re in for a treat.

SciPy is like a Swiss Army knife ๐Ÿ”ง for scientific computing - itโ€™s packed with tools for everything from signal processing to optimization, statistics to spatial algorithms. Whether youโ€™re analyzing experimental data ๐Ÿ“Š, simulating physical systems ๐ŸŒŠ, or solving differential equations ๐Ÿ“ˆ, SciPy has got your back!

By the end of this tutorial, youโ€™ll be solving real scientific problems with confidence. Letโ€™s embark on this scientific journey together! ๐Ÿš€

๐Ÿ“š Understanding SciPy

๐Ÿค” What is SciPy?

SciPy is like having a team of expert mathematicians and scientists in your computer! ๐Ÿงฎ Think of it as a powerful extension of NumPy that adds specialized tools for scientific and technical computing.

In Python terms, SciPy provides:

  • โœจ Advanced mathematical functions
  • ๐Ÿš€ Optimization algorithms
  • ๐Ÿ“Š Statistical tools
  • ๐ŸŒŠ Signal processing capabilities
  • ๐Ÿ›ก๏ธ Numerical integration and differentiation

๐Ÿ’ก Why Use SciPy?

Hereโ€™s why scientists and engineers love SciPy:

  1. Comprehensive Toolset ๐Ÿ”ง: Everything from linear algebra to image processing
  2. Battle-tested Algorithms ๐Ÿ’ช: Implementations used by researchers worldwide
  3. NumPy Integration ๐Ÿ”—: Seamless work with NumPy arrays
  4. Performance โšก: Optimized C and Fortran code under the hood

Real-world example: Imagine youโ€™re analyzing weather data ๐ŸŒก๏ธ. SciPy can help you interpolate missing values, find patterns, and even predict future temperatures!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Getting Started

Letโ€™s start with the essentials:

# ๐Ÿ‘‹ Hello, SciPy!
import numpy as np
from scipy import stats, optimize, signal
import matplotlib.pyplot as plt

# ๐ŸŽจ Create some sample data
data = np.random.normal(100, 15, 1000)  # Mean=100, std=15

# ๐Ÿ“Š Basic statistics
mean = np.mean(data)
std = np.std(data)
print(f"Mean: {mean:.2f} ๐Ÿ“Š")
print(f"Std Dev: {std:.2f} ๐Ÿ“ˆ")

๐Ÿ’ก Explanation: We import the modules we need and create some sample data. SciPy plays nicely with NumPy and Matplotlib!

๐ŸŽฏ Common SciPy Modules

Here are the modules youโ€™ll use most often:

# ๐Ÿ—๏ธ Key SciPy modules
from scipy import (
    stats,      # ๐Ÿ“Š Statistics
    optimize,   # ๐ŸŽฏ Optimization
    integrate,  # โˆซ Integration
    interpolate,# ๐Ÿ“ˆ Interpolation
    signal,     # ๐ŸŒŠ Signal processing
    linalg      # ๐Ÿ”ข Linear algebra
)

# ๐Ÿ”„ Example: Statistical test
data1 = np.random.normal(0, 1, 100)
data2 = np.random.normal(0.5, 1, 100)

# ๐Ÿงช T-test to compare means
t_stat, p_value = stats.ttest_ind(data1, data2)
print(f"T-statistic: {t_stat:.3f} ๐Ÿ“Š")
print(f"P-value: {p_value:.3f} ๐ŸŽฏ")

๐Ÿ’ก Practical Examples

๐ŸŽฏ Example 1: Curve Fitting for Scientific Data

Letโ€™s fit experimental data to a model:

# ๐Ÿงช Simulating experimental data
def exponential_decay(t, A, tau, C):
    """Exponential decay model: A * exp(-t/tau) + C"""
    return A * np.exp(-t/tau) + C

# ๐Ÿ“Š Generate "experimental" data with noise
time = np.linspace(0, 10, 100)
true_params = [5.0, 2.0, 1.0]  # A=5, tau=2, C=1
noise = np.random.normal(0, 0.1, len(time))
data = exponential_decay(time, *true_params) + noise

# ๐ŸŽฏ Fit the model to data
from scipy.optimize import curve_fit

popt, pcov = curve_fit(exponential_decay, time, data)
perr = np.sqrt(np.diag(pcov))

print("๐Ÿ“Š Fitted parameters:")
print(f"  A = {popt[0]:.3f} ยฑ {perr[0]:.3f}")
print(f"  tau = {popt[1]:.3f} ยฑ {perr[1]:.3f}")
print(f"  C = {popt[2]:.3f} ยฑ {perr[2]:.3f}")

# ๐ŸŽจ Visualize the fit
plt.figure(figsize=(10, 6))
plt.scatter(time, data, alpha=0.5, label='Data ๐Ÿ“Š')
plt.plot(time, exponential_decay(time, *popt), 'r-', 
         label='Fit ๐ŸŽฏ', linewidth=2)
plt.xlabel('Time (s) โฑ๏ธ')
plt.ylabel('Signal ๐Ÿ“ˆ')
plt.legend()
plt.title('Exponential Decay Fit ๐Ÿงช')
plt.grid(True, alpha=0.3)
plt.show()

๐ŸŽฏ Try it yourself: Modify the model to fit a damped oscillation instead!

๐ŸŒŠ Example 2: Signal Processing and Filtering

Letโ€™s clean up a noisy signal:

# ๐ŸŽต Create a noisy signal
fs = 1000  # Sampling frequency
t = np.linspace(0, 1, fs, endpoint=False)

# ๐ŸŽผ Signal: 50 Hz + 120 Hz + noise
signal_clean = np.sin(2*np.pi*50*t) + 0.5*np.sin(2*np.pi*120*t)
noise = 0.8 * np.random.randn(len(t))
signal_noisy = signal_clean + noise

# ๐Ÿ”ง Design a low-pass filter
from scipy.signal import butter, filtfilt

def butter_lowpass_filter(data, cutoff, fs, order=5):
    """Apply Butterworth low-pass filter"""
    nyq = 0.5 * fs  # Nyquist frequency
    normal_cutoff = cutoff / nyq
    b, a = butter(order, normal_cutoff, btype='low', analog=False)
    y = filtfilt(b, a, data)
    return y

# ๐ŸŽฏ Filter the signal
cutoff = 80  # Hz
filtered_signal = butter_lowpass_filter(signal_noisy, cutoff, fs)

# ๐Ÿ“Š Plot results
fig, axes = plt.subplots(3, 1, figsize=(10, 8))

# Original clean signal
axes[0].plot(t[:200], signal_clean[:200], 'b-', linewidth=2)
axes[0].set_title('Clean Signal ๐ŸŽต')
axes[0].grid(True, alpha=0.3)

# Noisy signal
axes[1].plot(t[:200], signal_noisy[:200], 'r-', alpha=0.7)
axes[1].set_title('Noisy Signal ๐Ÿ“ก')
axes[1].grid(True, alpha=0.3)

# Filtered signal
axes[2].plot(t[:200], filtered_signal[:200], 'g-', linewidth=2)
axes[2].set_title('Filtered Signal โœจ')
axes[2].set_xlabel('Time (s) โฑ๏ธ')
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# ๐Ÿ” Frequency analysis
from scipy.fft import fft, fftfreq

def plot_spectrum(signal, fs, title):
    """Plot frequency spectrum"""
    N = len(signal)
    yf = fft(signal)
    xf = fftfreq(N, 1/fs)[:N//2]
    
    plt.figure(figsize=(10, 4))
    plt.plot(xf, 2.0/N * np.abs(yf[:N//2]))
    plt.title(f'{title} ๐Ÿ“Š')
    plt.xlabel('Frequency (Hz) ๐ŸŽต')
    plt.ylabel('Amplitude ๐Ÿ“ˆ')
    plt.xlim(0, 200)
    plt.grid(True, alpha=0.3)
    plt.show()

plot_spectrum(signal_noisy, fs, 'Noisy Signal Spectrum')
plot_spectrum(filtered_signal, fs, 'Filtered Signal Spectrum')

๐Ÿ”ฌ Example 3: Scientific Optimization

Letโ€™s optimize a complex function:

# ๐ŸŽฏ Multi-dimensional optimization problem
def rosenbrock(x):
    """The Rosenbrock function - a classic test case"""
    return sum(100.0*(x[1:]-x[:-1]**2.0)**2.0 + (1-x[:-1])**2.0)

# ๐Ÿš€ Different optimization methods
from scipy.optimize import minimize

# Starting point
x0 = np.array([0, 0, 0, 0, 0])

print("๐Ÿ” Optimization Results:\n")

# Method 1: Nelder-Mead
result_nm = minimize(rosenbrock, x0, method='Nelder-Mead')
print(f"Nelder-Mead: {result_nm.fun:.6f} at {result_nm.x}")

# Method 2: BFGS
result_bfgs = minimize(rosenbrock, x0, method='BFGS')
print(f"BFGS: {result_bfgs.fun:.6f} at {result_bfgs.x}")

# Method 3: Powell
result_powell = minimize(rosenbrock, x0, method='Powell')
print(f"Powell: {result_powell.fun:.6f} at {result_powell.x}")

# ๐ŸŽจ Visualize 2D slice of the function
x = np.linspace(-2, 2, 100)
y = np.linspace(-1, 3, 100)
X, Y = np.meshgrid(x, y)
Z = np.zeros_like(X)

for i in range(len(x)):
    for j in range(len(y)):
        Z[j, i] = rosenbrock([X[j, i], Y[j, i]])

plt.figure(figsize=(10, 8))
contour = plt.contour(X, Y, Z, levels=50)
plt.colorbar(contour)
plt.plot(1, 1, 'r*', markersize=15, label='Global Minimum ๐ŸŽฏ')
plt.xlabel('X ๐Ÿ“')
plt.ylabel('Y ๐Ÿ“')
plt.title('Rosenbrock Function Contour Plot ๐Ÿ”๏ธ')
plt.legend()
plt.show()

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Numerical Integration

When you need to compute complex integrals:

# ๐ŸŽฏ Advanced integration techniques
from scipy import integrate

# Example 1: Definite integral
def integrand(x):
    """Function to integrate: e^(-x^2)"""
    return np.exp(-x**2)

# ๐Ÿ“ Different integration methods
result_quad, error = integrate.quad(integrand, -np.inf, np.inf)
print(f"Gaussian integral: {result_quad:.6f} ยฑ {error:.2e}")
print(f"Expected: {np.sqrt(np.pi):.6f} โœ“")

# Example 2: Double integral
def f(y, x):
    """2D function: x*y*exp(-x^2-y^2)"""
    return x * y * np.exp(-x**2 - y**2)

# ๐ŸŽฏ Integrate over a rectangle
result_dblquad, error = integrate.dblquad(
    f, 0, 1, 0, 1
)
print(f"\nDouble integral: {result_dblquad:.6f}")

# Example 3: ODE solving
def pendulum(y, t, b, c):
    """Damped pendulum equations"""
    theta, omega = y
    dydt = [omega, -b*omega - c*np.sin(theta)]
    return dydt

# ๐ŸŒŠ Solve the ODE
b = 0.25  # Damping
c = 5.0   # Frequency
y0 = [np.pi - 0.1, 0.0]  # Initial conditions
t = np.linspace(0, 10, 101)

sol = integrate.odeint(pendulum, y0, t, args=(b, c))

plt.figure(figsize=(10, 6))
plt.plot(t, sol[:, 0], 'b', label='Angle ฮธ ๐Ÿ“')
plt.plot(t, sol[:, 1], 'g', label='Angular velocity ฯ‰ ๐ŸŒ€')
plt.xlabel('Time โฑ๏ธ')
plt.ylabel('Value')
plt.title('Damped Pendulum Solution ๐ŸŽฏ')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

๐Ÿ—๏ธ Sparse Matrices and Linear Algebra

For large-scale scientific computing:

# ๐Ÿš€ Working with sparse matrices
from scipy.sparse import csr_matrix, linalg as sparse_linalg

# Create a large sparse matrix
size = 1000
data = np.random.rand(3000)
rows = np.random.randint(0, size, 3000)
cols = np.random.randint(0, size, 3000)

sparse_matrix = csr_matrix((data, (rows, cols)), shape=(size, size))

print(f"Matrix size: {size}x{size} ๐Ÿ“")
print(f"Non-zero elements: {sparse_matrix.nnz} ๐ŸŽฏ")
print(f"Sparsity: {sparse_matrix.nnz / (size**2) * 100:.2f}% ๐Ÿ“Š")

# ๐Ÿ”ข Solve sparse linear system
b = np.random.rand(size)
x = sparse_linalg.spsolve(sparse_matrix, b)

# Verify solution
residual = np.linalg.norm(sparse_matrix.dot(x) - b)
print(f"Solution residual: {residual:.2e} โœ…")

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Wrong Function Arguments

# โŒ Wrong way - incorrect argument order
from scipy.optimize import fsolve

def equation(x, a, b):
    return a * x**2 + b

# This will fail!
# solution = fsolve(equation, 0, args=(2, 3))  # ๐Ÿ’ฅ Error!

# โœ… Correct way - proper function signature
def equation_correct(x, a, b):
    return a * x**2 + b

# Use lambda or partial for additional arguments
from functools import partial
equation_with_params = partial(equation_correct, a=2, b=3)
solution = fsolve(equation_with_params, 0)
print(f"Solution: x = {solution[0]:.3f} โœ…")

๐Ÿคฏ Pitfall 2: Convergence Issues

# โŒ Poor initial guess
def complex_function(x):
    return np.cos(x) - x

# Bad initial guess might not converge
# result = fsolve(complex_function, 10)  # ๐Ÿ’ฅ May not converge!

# โœ… Better approach - try multiple starting points
initial_guesses = [0, 1, -1, 0.5]
solutions = []

for guess in initial_guesses:
    try:
        sol = fsolve(complex_function, guess)[0]
        if abs(complex_function(sol)) < 1e-10:
            solutions.append(sol)
            print(f"Found solution: {sol:.6f} โœ…")
    except:
        print(f"Failed with guess: {guess} โš ๏ธ")

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Choose the Right Tool: Use specialized functions for specific problems
  2. ๐Ÿ“Š Validate Results: Always check convergence and residuals
  3. โšก Consider Performance: Use sparse matrices for large problems
  4. ๐Ÿ›ก๏ธ Handle Edge Cases: Check for singularities and numerical issues
  5. ๐Ÿ“– Read the Docs: SciPy has excellent documentation - use it!

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Scientific Data Analyzer

Create a tool that analyzes experimental data:

๐Ÿ“‹ Requirements:

  • โœ… Load data from CSV file
  • ๐Ÿ“Š Perform statistical analysis (mean, std, confidence intervals)
  • ๐ŸŽฏ Fit data to a model (your choice)
  • ๐ŸŒŠ Apply signal filtering if needed
  • ๐Ÿ“ˆ Create publication-quality plots

๐Ÿš€ Bonus Points:

  • Add hypothesis testing
  • Implement bootstrap confidence intervals
  • Create an interactive parameter explorer

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
# ๐ŸŽฏ Scientific Data Analyzer
import pandas as pd
from scipy import stats, optimize, signal
import matplotlib.pyplot as plt
import numpy as np

class ScientificAnalyzer:
    def __init__(self, data_path=None):
        """Initialize the analyzer"""
        self.data = None
        self.results = {}
        
        if data_path:
            self.load_data(data_path)
    
    def generate_sample_data(self):
        """Generate sample experimental data"""
        np.random.seed(42)
        time = np.linspace(0, 10, 200)
        
        # Simulated experimental signal
        true_signal = 5 * np.exp(-time/3) * np.cos(2*np.pi*time)
        noise = np.random.normal(0, 0.5, len(time))
        measured_signal = true_signal + noise
        
        self.data = pd.DataFrame({
            'time': time,
            'signal': measured_signal,
            'true_signal': true_signal
        })
        print("๐Ÿ“Š Generated sample data!")
        
    def statistical_analysis(self):
        """Perform comprehensive statistical analysis"""
        if self.data is None:
            print("โŒ No data loaded!")
            return
        
        signal = self.data['signal'].values
        
        # Basic statistics
        self.results['mean'] = np.mean(signal)
        self.results['std'] = np.std(signal, ddof=1)
        self.results['median'] = np.median(signal)
        
        # Confidence interval (95%)
        confidence = 0.95
        n = len(signal)
        se = stats.sem(signal)
        interval = stats.t.interval(confidence, n-1, 
                                   loc=self.results['mean'], 
                                   scale=se)
        self.results['ci_95'] = interval
        
        # Normality test
        _, p_value = stats.normaltest(signal)
        self.results['normality_p'] = p_value
        
        print("\n๐Ÿ“Š Statistical Analysis Results:")
        print(f"Mean: {self.results['mean']:.3f}")
        print(f"Std Dev: {self.results['std']:.3f}")
        print(f"95% CI: [{interval[0]:.3f}, {interval[1]:.3f}]")
        print(f"Normality test p-value: {p_value:.3f}")
        
    def fit_model(self, model_func=None):
        """Fit data to a model"""
        if self.data is None:
            print("โŒ No data loaded!")
            return
            
        # Default model: damped oscillation
        if model_func is None:
            def model_func(t, A, tau, omega, phi):
                return A * np.exp(-t/tau) * np.cos(omega*t + phi)
        
        time = self.data['time'].values
        signal = self.data['signal'].values
        
        # Initial parameter guess
        p0 = [5, 3, 2*np.pi, 0]
        
        try:
            popt, pcov = optimize.curve_fit(model_func, time, signal, p0=p0)
            self.results['fit_params'] = popt
            self.results['fit_errors'] = np.sqrt(np.diag(pcov))
            self.results['fit_function'] = model_func
            
            # Calculate R-squared
            fitted = model_func(time, *popt)
            residuals = signal - fitted
            ss_res = np.sum(residuals**2)
            ss_tot = np.sum((signal - np.mean(signal))**2)
            r_squared = 1 - (ss_res / ss_tot)
            self.results['r_squared'] = r_squared
            
            print(f"\n๐ŸŽฏ Model Fit Results:")
            print(f"Rยฒ = {r_squared:.4f}")
            print("Parameters:")
            param_names = ['A', 'tau', 'omega', 'phi']
            for name, val, err in zip(param_names, popt, self.results['fit_errors']):
                print(f"  {name} = {val:.3f} ยฑ {err:.3f}")
                
        except Exception as e:
            print(f"โŒ Fitting failed: {e}")
            
    def filter_signal(self, method='lowpass', cutoff=2):
        """Apply signal filtering"""
        if self.data is None:
            print("โŒ No data loaded!")
            return
            
        signal = self.data['signal'].values
        fs = 1 / (self.data['time'].iloc[1] - self.data['time'].iloc[0])
        
        if method == 'lowpass':
            b, a = signal.butter(4, cutoff, fs=fs, btype='low')
            filtered = signal.filtfilt(b, a, signal)
        elif method == 'savgol':
            filtered = signal.savgol_filter(signal, 21, 3)
        else:
            print(f"โŒ Unknown filter method: {method}")
            return
            
        self.data['filtered_signal'] = filtered
        print(f"โœ… Applied {method} filter!")
        
    def visualize_results(self):
        """Create publication-quality plots"""
        if self.data is None:
            print("โŒ No data loaded!")
            return
            
        fig, axes = plt.subplots(2, 2, figsize=(12, 10))
        
        # 1. Raw data
        ax = axes[0, 0]
        ax.plot(self.data['time'], self.data['signal'], 
                'b.', alpha=0.5, label='Measured')
        if 'true_signal' in self.data:
            ax.plot(self.data['time'], self.data['true_signal'], 
                    'r-', label='True', linewidth=2)
        ax.set_xlabel('Time (s)')
        ax.set_ylabel('Signal')
        ax.set_title('๐Ÿ“Š Raw Data')
        ax.legend()
        ax.grid(True, alpha=0.3)
        
        # 2. Statistical distribution
        ax = axes[0, 1]
        ax.hist(self.data['signal'], bins=30, density=True, 
                alpha=0.7, color='blue', edgecolor='black')
        
        # Overlay normal distribution
        x = np.linspace(self.data['signal'].min(), 
                       self.data['signal'].max(), 100)
        ax.plot(x, stats.norm.pdf(x, self.results['mean'], 
                                 self.results['std']), 
                'r-', linewidth=2, label='Normal fit')
        ax.set_xlabel('Signal Value')
        ax.set_ylabel('Density')
        ax.set_title('๐Ÿ“ˆ Distribution')
        ax.legend()
        
        # 3. Model fit
        ax = axes[1, 0]
        if 'fit_function' in self.results:
            time = self.data['time'].values
            fitted = self.results['fit_function'](time, 
                                                 *self.results['fit_params'])
            ax.plot(time, self.data['signal'], 'b.', 
                   alpha=0.5, label='Data')
            ax.plot(time, fitted, 'r-', linewidth=2, 
                   label=f'Fit (Rยฒ={self.results["r_squared"]:.3f})')
            ax.set_xlabel('Time (s)')
            ax.set_ylabel('Signal')
            ax.set_title('๐ŸŽฏ Model Fit')
            ax.legend()
            ax.grid(True, alpha=0.3)
        
        # 4. Filtered signal
        ax = axes[1, 1]
        if 'filtered_signal' in self.data:
            ax.plot(self.data['time'], self.data['signal'], 
                   'b-', alpha=0.3, label='Original')
            ax.plot(self.data['time'], self.data['filtered_signal'], 
                   'r-', linewidth=2, label='Filtered')
            ax.set_xlabel('Time (s)')
            ax.set_ylabel('Signal')
            ax.set_title('๐ŸŒŠ Filtered Signal')
            ax.legend()
            ax.grid(True, alpha=0.3)
        
        plt.tight_layout()
        plt.show()

# ๐ŸŽฎ Test it out!
analyzer = ScientificAnalyzer()
analyzer.generate_sample_data()
analyzer.statistical_analysis()
analyzer.fit_model()
analyzer.filter_signal(method='savgol')
analyzer.visualize_results()

๐ŸŽ“ Key Takeaways

Youโ€™ve mastered scientific computing with SciPy! Hereโ€™s what you can now do:

  • โœ… Perform statistical analyses with confidence ๐Ÿ“Š
  • โœ… Fit models to experimental data like a pro ๐ŸŽฏ
  • โœ… Process and filter signals effectively ๐ŸŒŠ
  • โœ… Solve optimization problems efficiently ๐Ÿš€
  • โœ… Handle numerical computations with ease ๐Ÿงฎ

Remember: SciPy is your scientific computing companion - it makes complex calculations simple and reliable! ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve unlocked the power of scientific computing with SciPy!

Hereโ€™s what to explore next:

  1. ๐Ÿ’ป Practice with real experimental data
  2. ๐Ÿ—๏ธ Build a complete data analysis pipeline
  3. ๐Ÿ“š Dive into specialized SciPy modules (spatial, ndimage, etc.)
  4. ๐ŸŒŸ Combine SciPy with machine learning libraries

Keep experimenting, keep discovering, and remember - every great scientific discovery started with curious exploration! ๐Ÿš€


Happy scientific computing! ๐ŸŽ‰๐Ÿ”ฌโœจ