toolsrtm.inversion
Trait-inversion tools: CARS-PLS and VIF predictor selection, LUT nearest-
neighbour (“merit function”) matching, and a 12-algorithm ML dispatcher
(PLSR/SVM/RF/GB/NN/Bayesian/AdaBag/BRNN/xGB/RVM/qLASSO/Ensemble) built on
scikit-learn/xgboost. Direct port of ToolsRTM::carspls/get.cars.pls,
getVIF, get.inversionOpt, get.inversion, hybrid_inversion/
hybrid_inversionE.
Note
Needs the optional ml extra: pip install "toolsrtm[ml]". Nothing
in this module is imported by toolsrtm/__init__.py’s own import chain
– a plain import toolsrtm never requires scikit-learn/xgboost.
Note
R’s get.inversion/hybrid_inversion dispatch to specific caret
methods (bartMachine, rqlasso, rvmLinear, AdaBag, brnn,
…). See ALGORITHMS for exactly which
scikit-learn/xgboost estimator each algorithm name maps to, and, where
there’s no direct equivalent, what was substituted and why.
Quick example
import numpy as np, pandas as pd
from toolsrtm import foursail
from toolsrtm.inversion import get_inversion
rng = np.random.default_rng(1)
rows = []
for _ in range(200):
Cab, LAI = rng.uniform(10, 80), rng.uniform(0.5, 6)
inputLUT = dict(N=1.5, Cab=Cab, Car=8, Anth=1, Cbrown=0, EWT=0.01, LMA=0.009, alpha=40,
LIDFa=-0.35, LIDFb=-0.15, TypeLidf=1,
LAI=LAI, hspot=0.01, tts=30, tto=0, psi=0)
sail = foursail(inputLUT, np.full(2101, 0.15), leaf_model="PROSPECT-D", spectrum_all=True)
row = {"Cab": Cab, "LAI": LAI}
for wl in (490, 560, 665, 705, 740, 783, 842, 865, 1610, 2190):
row[f"R{wl}"] = sail.rsot[wl - 400]
rows.append(row)
df = pd.DataFrame(rows)
band_cols = [c for c in df.columns if c.startswith("R")]
result = get_inversion(df, dep_var="Cab", inputs=band_cols, algorithm="PLSR", n_samples=200, seed=1)
print(result.statistics["test"]["r2"]) # held-out test R2
Input get_inversion() Output
--------------------------- ---------------------- ---------------------------
df [n rows] LUT: predictor result.model fitted estimator
bands + dep_var --------------------> result.statistics train/test R2, RMSE
dep_var = trait to invert (e.g. "Cab") result.predictions test-set predicted
inputs = predictor column names vs. observed
algorithm = "PLSR"/"RF"/"SVM"/... (see ALGORITHMS)
Trait-inversion tools: CARS-PLS feature selection, VIF-based collinearity pruning, LUT nearest-neighbour (“merit function”) inversion, and a multi-algorithm ML dispatcher.
Python port of ToolsRTM/R/carspls.R / get.cars.pls.R, getVIF.R,
get.inversion.R, hybrid_inversion.R / hybrid_inversionE.R, and
get.inversionOpt.R. Needs the optional ml extra
(pip install toolsrtm[ml]: scikit-learn, xgboost) – none of these
functions are imported by toolsrtm/__init__.py at import time, and each
imports its own ML dependencies lazily so a plain import toolsrtm never
requires scikit-learn/xgboost to be installed.
R’s get.inversion/hybrid_inversion dispatch by name to specific
caret methods (bartMachine, rqlasso, rvmLinear, AdaBag, brnn, …). Several
of those have no direct scikit-learn/xgboost equivalent; where that’s the
case the docstring of the relevant function says exactly which estimator was
substituted and why. Unlike the pure radiative-transfer math ported
elsewhere in this package, none of this module is verified to floating-point
precision against R – caret’s own cross-validated tuning is stochastic,
so a Python port using different (but comparable) estimators and search
grids will not reproduce R’s numbers bit-for-bit even in principle. What’s
verified instead: each algorithm runs end-to-end on held-out data and
produces sane, comparable-magnitude accuracy metrics (see
tests/test_inversion.py).
- class toolsrtm.inversion.CarsPlsResult(coef, n_var, rmsecv, num_lv, optimal_iteration, min_error, selected_variables)[source]
Bases:
objectResult of
carspls(). Mirrors the RCARSlist 1:1 exceptselected_variablesis 0-indexed (Python) instead of 1-indexed (R).- Parameters:
- optimal_iteration: int
1-indexed iteration with the lowest RMSECV (matches R)
- min_error: float
- toolsrtm.inversion.carspls(X, y, n_lv=2, fold=10, scale_pretreat=True, iteration=50, partition_type='interleaved', verbose=False)[source]
Competitive Adaptive Reweighted Sampling for PLS variable selection.
Python port of
carspls/get.cars.pls(R, original algorithm by Yizeng Liang & Hongdong Li, MATLAB->R port by Hongdong Li 2009). At each ofiterationrounds: fits a PLS model on the currently-retained variables, cross-validates it to get an RMSECV curve over 1..n_lv components, records the coefficient-magnitude-ranked variable importance, and forcibly eliminates the lowest-ranked variables via an exponentially decreasing retention schedule (Monte-Carlo/EDF sampling). The iteration with the lowest RMSECV gives the final selected variable set.- Parameters:
X (ndarray) – (n_samples, n_vars) predictor matrix.
y (ndarray) – (n_samples,) response vector.
n_lv (int) – number of PLS latent variables (components) to fit/tune over.
fold (int) – number of cross-validation segments.
scale_pretreat (bool) – if True, scale (not just center) each predictor.
iteration (int) – number of CARS-PLS elimination rounds.
partition_type (Literal['interleaved', 'consecutive', 'random']) – cross-validation fold assignment:
"interleaved"(round-robin),"consecutive"(contiguous blocks), or"random".verbose (bool) – print progress per iteration (matches R’s own screen output).
- Returns:
- Return type:
- toolsrtm.inversion.get_vif(frame, columns=None, thresh=10.0, trace=True)[source]
Backward-elimination variable selection by Variance Inflation Factor.
Python port of
getVIF(R, VIF function originally from https://beckmw.wordpress.com/2013/02/05/collinearity-and-stepwise-vif-selection/). Iteratively regresses each remaining variable on all others; drops the variable with the highest VIF (1 / (1 - R^2)) as long as any VIF exceedsthresh.- Parameters:
frame (ndarray) – (n_samples, n_vars) array, or a pandas.DataFrame.
columns (Sequence[str] | None) – variable names, required if
frameis a bare array; ignored (and taken fromframe.columns) ifframeis a DataFrame.thresh (float) – VIF threshold above which a variable is flagged as collinear.
trace (bool) – print each elimination step (matches R’s own console output).
- Returns:
names (or 0-indexed positions, if
columnsis None andframeis a bare array) of the retained variables.- Return type:
list[str] | list[int]
- class toolsrtm.inversion.InversionOptResult(rfl_best: 'np.ndarray', lut_best: "'pd.DataFrame'")[source]
Bases:
object- Parameters:
rfl_best (np.ndarray)
lut_best (pd.DataFrame)
- rfl_best: np.ndarray
(n_obs, n_wave) best-matching (n_opt-averaged) simulated spectra
- lut_best: pd.DataFrame
(n_obs, n_lut_columns) n_opt-averaged LUT parameters per observation
- toolsrtm.inversion.get_inversion_opt(rfl_sensor, rfl_rtm, lut, wave=None, method='merit-RMSE', n_opt=1, custom_stat=None)[source]
LUT (look-up table) inversion by nearest-neighbour spectral matching.
Python port of
get.inversionOpt(R). For each observed spectrum inrfl_sensor, ranks every simulated spectrum inrfl_rtmby a merit (error) function and averages then_optbest matches’ LUT parameters and reflectance. Fully vectorized (broadcasts each observation against the whole LUT at once) rather than R’s nested per-row loop – same algorithm, no numerical differences expected for the built-in merit functions (verified against a hand-computed reference below).- Parameters:
rfl_sensor (ndarray) – (n_obs, n_wave) observed/sensor reflectance.
rfl_rtm (ndarray) – (n_lut, n_wave) simulated reflectance from the LUT.
lut – (n_lut, n_params) pandas.DataFrame of the LUT’s input parameters.
wave (Sequence[float] | None) – wavelengths corresponding to columns of
rfl_sensor/rfl_rtm(only used to name the returned reflectance columns; optional).method (str) – one of
"merit-RMSE","merit-NRMSE","merit-MAE","merit-NMB","merit-FGE", or"merit-custom.metric"(requirescustom_stat).n_opt (int) – number of best-matching LUT rows to average per observation.
custom_stat (Callable[[ndarray, ndarray], ndarray] | None) – optional
f(sim, obs) -> errormerit function, broadcast over the last axis exactly like the built-in ones; overridesmethod.
- Returns:
- Return type:
- toolsrtm.inversion.ALGORITHMS = {'AdaBag': "sklearn AdaBoostRegressor over shallow DecisionTreeRegressor stumps (matches caret method 'AdaBag')", 'BRNN': "sklearn MLPRegressor with strong L2 (alpha) regularization -- substitute: approximates 'Bayesian regularization' via explicit weight decay, not R's brnn Gauss-Newton/Levenberg-Marquardt fit", 'Bayesian': "sklearn BayesianRidge -- substitute: no BART implementation in sklearn/xgboost; BayesianRidge is a Bayesian *linear* model, not R's bartMachine (Bayesian additive trees)", 'Ensemble': "sklearn StackingRegressor(GB + SVR + MLP, final_estimator=LinearRegression) (matches caretEnsemble::caretStack(..., method='glm'))", 'GB': "sklearn GradientBoostingRegressor (matches caret method 'gbm')", 'NN': "sklearn MLPRegressor, single hidden layer (matches caret method 'nnet')", 'PLSR': "sklearn PLSRegression, n_components tuned by 5-fold CV (matches caret method 'pls')", 'RF': "sklearn RandomForestRegressor, max_features tuned (matches caret method 'rf')", 'RVM': "sklearn BayesianRidge -- substitute: no Relevance Vector Machine in sklearn/xgboost; BayesianRidge shares RVM's sparsity-favouring linear-Bayesian character", 'SVM': "sklearn SVR(kernel='rbf'), gamma/C tuned by grid search (matches caret method 'svmRadial' via e1071::tune.svm)", 'qLASSO': "sklearn QuantileRegressor(quantile=0.5, solver='highs'), L1-penalized (matches caret method 'rqlasso')", 'xGB': "xgboost XGBRegressor(booster='gblinear') (matches caret method 'xgbLinear')"}
R’s caret-method name each algorithm dispatches to, and, where scikit-learn/ xgboost has no direct equivalent, the substitution actually used here.
- class toolsrtm.inversion.InversionResult(model_label: 'str', model: 'object', predictions: 'dict', statistics: 'dict', importance: 'dict | None')[source]
Bases:
object- Parameters:
model_label (str)
model (object)
predictions (dict)
statistics (dict)
importance (dict | None)
- model_label: str
- model: object
- predictions: dict
np.ndarray, “test”: np.ndarray}
- Type:
{“train”
- statistics: dict
{“r2”:.., “rmse”:.., “mae”:..}, “test”: {…}}
- Type:
{“train”
- importance: dict | None
importance}, or None if not available for this algorithm
- Type:
{input_name
- toolsrtm.inversion.get_inversion(data, dep_var, inputs, algorithm='PLSR', seed=123, n_samples=500, test_size=0.3)[source]
Fit and evaluate a plant-trait inversion model with one of 12 algorithms, on a held-out train/test split.
Python port of
get.inversion(R). SeeALGORITHMSfor exactly which scikit-learn/xgboost estimator eachalgorithmname dispatches to, and, for the 4 algorithms with no direct equivalent (Bayesian,BRNN,RVM– and noteAdaBag/xGB/qLASSOdo have close matches), what was substituted and why. Unlike R’s version, tuning here is a single small grid search per algorithm (not caret’s full repeated-CV search), to keep this runnable as a demo rather than a multi-hour job – the same design choice already used byScripts/Python/*/2_inversion_ml.py.- Parameters:
data – pandas.DataFrame containing
dep_varand allinputscolumns.dep_var (str) – name of the response column to predict.
inputs (Sequence[str]) – names of the predictor columns.
algorithm (str) – one of the keys of
ALGORITHMS.seed (int) – random seed for the train/test split and any stochastic estimator.
n_samples (int | None) – if given and less than
len(data), randomly subsample this many rows before splitting (matches R’s own tuning-sample-size argument).test_size (float) – fraction of (sub-sampled) data held out for testing.
- Returns:
- Return type:
- class toolsrtm.inversion.HybridInversionResult(model: 'object', keep_variables: 'list[str]', statistics: "'pd.DataFrame'", predictions: 'dict')[source]
Bases:
object- Parameters:
model (object)
keep_variables (list[str])
statistics (pd.DataFrame)
predictions (dict)
- model: object
- keep_variables: list[str]
predictor columns actually used, after pattern/collinearity selection
- statistics: pd.DataFrame
rows “train”/”test” (+ “field” if field_data given), columns r2/rmse/mae
- predictions: dict
np.ndarray, “test”: np.ndarray[, “field”: np.ndarray]}, original (untransformed) scale
- Type:
{“train”
- toolsrtm.inversion.hybrid_inversion(lut, input, split=0.8, seed=None, method=None, collinearity=None, pattern=None, trans=True, field_data=None, acron=None)[source]
Fit a single-algorithm trait-inversion model with optional predictor selection (by name pattern, then optionally VIF or CARS-PLS pruning) and an optional log-transform of the response.
Python port of
hybrid_inversion(R).methoddispatches to the same 5 estimators asget_inversion()’sSVM/RF/GB/NN/Ensemble("nnet"here maps to"NN"there, matching R’s own caret method name) – seeALGORITHMSfor what each one is. Note: R’s train/test split usescaret::createDataPartition(percentile- stratified on the response); this port uses a plain random split via scikit-learn, which is not percentile-stratified – a documented approximation, not expected to change results materially for the LUT-sized (typically hundreds of rows) datasets this is meant for.- Parameters:
lut – pandas.DataFrame with the response column
inputand candidate predictor columns.input (str) – name of the response column to predict.
split (float) – train-fraction of the train/test split (R’s own convention; note this is the train fraction, unlike
get_inversion()’stest_size).seed (int | None) – random seed.
method (str | None) – one of
"SVM","RF","GB","nnet","Ensemble". Defaults to"SVM"(matches R’s own default).collinearity (Literal['VIF', 'CARS'] | None) –
None(use everypattern-matched column),"VIF"(prune viaget_vif()), or"CARS"(select viacarspls()).pattern (str | None) – substring that predictor column names must contain (e.g.
"B"for reflectance-band columns namedB1,B2, …); ifNone, every column exceptinputis a candidate.trans (bool) – log-transform
inputbefore fitting (matches R’s own default); predictions/statistics are reported back on the original scale.field_data – optional pandas.DataFrame of field observations to validate against, in addition to the LUT’s own test split.
acron (str | None) – suffix appended to
inputto find the observed column infield_data(e.g.acron="_obsv"looks forf"{input}_obsv"). Required iffield_datais given.
- Returns:
- Return type:
- toolsrtm.inversion.hybrid_inversion_ensemble(lut, input, split=0.8, seed=None, collinearity=None, pattern=None, field_data=None, acron=None)[source]
hybrid_inversion()withmethod="Ensemble"fixed.Python port of
hybrid_inversionE(R) – the R function ishybrid_inversionwith the model choice hardcoded to the 3-model (GB + SVM + neural net) stacking ensemble and notrans/log-transform option, which this wrapper matches (trans=False).- Parameters:
input (str)
split (float)
seed (int | None)
collinearity (Literal['VIF', 'CARS'] | None)
pattern (str | None)
acron (str | None)
- Return type: