File size: 6,854 Bytes
e2b220f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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
125
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
"""
Physics-informed feature engineering for femtosecond laser processing.

Derives key physical quantities from raw laser parameters following
established laser-material interaction physics:
- Pulse energy from average power and repetition rate
- Peak fluence from pulse energy and beam geometry (Gaussian beam)
- Effective pulse number from scanning parameters
- Spatial overlap from scan speed and spot size

References
----------
- Perry et al., J. Biomed. Opt. (2009) - fs-laser ablation of hydrogels
- Gamaly et al., Physics Reports (2002) - fs-laser ablation theory
- Petit et al., JLMN (2025) - physics-informed ML features for laser machining
"""

from __future__ import annotations

import logging
from typing import Optional

import numpy as np
import pandas as pd

logger = logging.getLogger(__name__)


def compute_pulse_energy(power_mW: np.ndarray, rep_rate_kHz: np.ndarray) -> np.ndarray:
    """
    Compute pulse energy from average power and repetition rate.

    E_pulse = P_avg / f_rep

    Parameters
    ----------
    power_mW : array-like
        Average laser power in milliwatts
    rep_rate_kHz : array-like
        Pulse repetition rate in kilohertz

    Returns
    -------
    np.ndarray
        Pulse energy in microjoules (µJ)
    """
    return np.asarray(power_mW) / np.maximum(np.asarray(rep_rate_kHz), 1e-6)


def compute_peak_fluence(
    pulse_energy_uJ: np.ndarray,
    spot_diameter_um: np.ndarray,
) -> np.ndarray:
    """
    Compute peak fluence for a Gaussian beam profile.

    F_peak = 2 * E_pulse / (π * w₀²)

    where w₀ = spot_diameter / 2 is the beam waist radius.

    Parameters
    ----------
    pulse_energy_uJ : array-like
        Pulse energy in microjoules
    spot_diameter_um : array-like
        Beam spot diameter (1/e²) in micrometers

    Returns
    -------
    np.ndarray
        Peak fluence in J/cm²
    """
    E_J = np.asarray(pulse_energy_uJ) * 1e-6  # µJ → J
    r_cm = np.asarray(spot_diameter_um) / 2 * 1e-4  # µm → cm (radius)
    return 2 * E_J / (np.pi * np.maximum(r_cm, 1e-8) ** 2)


def compute_effective_pulses(
    rep_rate_kHz: np.ndarray,
    spot_diameter_um: np.ndarray,
    scan_speed_mm_s: np.ndarray,
) -> np.ndarray:
    """
    Compute effective number of pulses per spot.

    N_eff = f_rep × d_spot / v_scan

    This represents the average number of laser pulses that irradiate
    each point along the scan path.

    Parameters
    ----------
    rep_rate_kHz : array-like
        Repetition rate in kHz
    spot_diameter_um : array-like
        Spot diameter in µm
    scan_speed_mm_s : array-like
        Scanning speed in mm/s

    Returns
    -------
    np.ndarray
        Effective pulse count (dimensionless)
    """
    rep_Hz = np.asarray(rep_rate_kHz) * 1000
    spot_mm = np.asarray(spot_diameter_um) * 1e-3
    speed = np.maximum(np.asarray(scan_speed_mm_s), 1e-6)
    return rep_Hz * spot_mm / speed


def compute_spatial_overlap(
    scan_speed_mm_s: np.ndarray,
    rep_rate_kHz: np.ndarray,
    spot_diameter_um: np.ndarray,
) -> np.ndarray:
    """
    Compute spatial pulse overlap percentage.

    Overlap = (1 - pitch / d_spot) × 100%

    where pitch = v_scan / f_rep is the distance between consecutive pulses.

    Parameters
    ----------
    scan_speed_mm_s : array-like
    rep_rate_kHz : array-like
    spot_diameter_um : array-like

    Returns
    -------
    np.ndarray
        Overlap percentage [0, 100)
    """
    pitch_um = np.asarray(scan_speed_mm_s) * 1000 / (np.asarray(rep_rate_kHz) * 1000)
    # pitch in µm: (mm/s * 1000 µm/mm) / (kHz * 1000 Hz/kHz) = µm
    # Actually: pitch = v/f = (mm/s) / (kHz*1000) = mm, convert to µm
    pitch_um = np.asarray(scan_speed_mm_s) / (np.asarray(rep_rate_kHz) * 1000) * 1000  # mm→µm
    spot = np.maximum(np.asarray(spot_diameter_um), 1e-6)
    overlap = np.clip(1 - pitch_um / spot, 0, 0.999)
    return overlap * 100


def compute_energy_dose(
    peak_fluence: np.ndarray,
    effective_pulses: np.ndarray,
) -> np.ndarray:
    """
    Compute total energy dose (accumulated fluence).

    Dose = F_peak × N_eff

    Parameters
    ----------
    peak_fluence : array-like
        Peak fluence in J/cm²
    effective_pulses : array-like
        Effective pulse number

    Returns
    -------
    np.ndarray
        Energy dose in J/cm²
    """
    return np.asarray(peak_fluence) * np.asarray(effective_pulses)


def compute_all_derived_features(df: pd.DataFrame) -> pd.DataFrame:
    """
    Compute all physics-informed derived features and add to DataFrame.

    Parameters
    ----------
    df : pd.DataFrame
        DataFrame with raw laser parameters

    Returns
    -------
    pd.DataFrame
        DataFrame with added derived feature columns
    """
    df = df.copy()

    # Pulse energy
    if "pulse_energy_uJ" not in df.columns:
        df["pulse_energy_uJ"] = compute_pulse_energy(
            df["power_mW"].values, df["repetition_rate_kHz"].values
        )
        logger.info("Computed: pulse_energy_uJ")

    # Peak fluence
    if "peak_fluence_J_cm2" not in df.columns:
        df["peak_fluence_J_cm2"] = compute_peak_fluence(
            df["pulse_energy_uJ"].values, df["spot_diameter_um"].values
        )
        logger.info("Computed: peak_fluence_J_cm2")

    # Effective pulses
    if "effective_pulses" not in df.columns:
        df["effective_pulses"] = compute_effective_pulses(
            df["repetition_rate_kHz"].values,
            df["spot_diameter_um"].values,
            df["scan_speed_mm_s"].values,
        )
        logger.info("Computed: effective_pulses")

    # Overlap
    if "overlap_percent" not in df.columns:
        df["overlap_percent"] = compute_spatial_overlap(
            df["scan_speed_mm_s"].values,
            df["repetition_rate_kHz"].values,
            df["spot_diameter_um"].values,
        )
        logger.info("Computed: overlap_percent")

    # Energy dose
    if "energy_dose_J_cm2" not in df.columns:
        df["energy_dose_J_cm2"] = compute_energy_dose(
            df["peak_fluence_J_cm2"].values, df["effective_pulses"].values
        )
        logger.info("Computed: energy_dose_J_cm2")

    return df


def get_feature_groups(config: dict) -> dict:
    """
    Get feature groups from configuration for ablation studies.

    Parameters
    ----------
    config : dict
        Experiment configuration dictionary

    Returns
    -------
    dict
        Mapping from group name to list of feature column names
    """
    data_cfg = config["data"]
    return {
        "laser": data_cfg["laser_features"],
        "material": data_cfg["material_features"],
        "derived": data_cfg["derived_features"],
        "all": (
            data_cfg["laser_features"]
            + data_cfg["material_features"]
            + data_cfg["derived_features"]
        ),
    }