adding more nodes
This commit is contained in:
98
backend/nodes/relate_fields.py
Normal file
98
backend/nodes/relate_fields.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""Relate two fields — fit functional relationships between two data fields."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from backend.node_registry import register_node
|
||||
from backend.data_types import DataField, RecordTable
|
||||
|
||||
|
||||
@register_node(display_name="Relate Fields")
|
||||
class RelateFields:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"field_a": ("DATA_FIELD",),
|
||||
"field_b": ("DATA_FIELD",),
|
||||
"function": (["linear", "quadratic", "cubic", "power", "logarithmic"],
|
||||
{"default": "linear"}),
|
||||
}
|
||||
}
|
||||
|
||||
OUTPUTS = (
|
||||
('DATA_FIELD', 'predicted'),
|
||||
('RECORD_TABLE', 'fit_params'),
|
||||
)
|
||||
FUNCTION = "process"
|
||||
|
||||
DESCRIPTION = (
|
||||
"Fit a functional relationship between two data fields: b = f(a). "
|
||||
"Outputs the predicted field_b from the fit and a table of fitted "
|
||||
"parameters with R² goodness-of-fit. "
|
||||
)
|
||||
|
||||
def process(self, field_a: DataField, field_b: DataField,
|
||||
function: str) -> tuple:
|
||||
a = np.asarray(field_a.data, dtype=np.float64).ravel()
|
||||
b = np.asarray(field_b.data, dtype=np.float64).ravel()
|
||||
n = min(len(a), len(b))
|
||||
a, b = a[:n], b[:n]
|
||||
|
||||
records = RecordTable()
|
||||
|
||||
if function == "linear":
|
||||
coeffs = np.polyfit(a, b, 1)
|
||||
predicted = np.polyval(coeffs, a)
|
||||
records.append({"quantity": "slope", "value": f"{coeffs[0]:.6g}", "unit": ""})
|
||||
records.append({"quantity": "intercept", "value": f"{coeffs[1]:.6g}", "unit": ""})
|
||||
|
||||
elif function == "quadratic":
|
||||
coeffs = np.polyfit(a, b, 2)
|
||||
predicted = np.polyval(coeffs, a)
|
||||
for i, name in enumerate(["a2", "a1", "a0"]):
|
||||
records.append({"quantity": name, "value": f"{coeffs[i]:.6g}", "unit": ""})
|
||||
|
||||
elif function == "cubic":
|
||||
coeffs = np.polyfit(a, b, 3)
|
||||
predicted = np.polyval(coeffs, a)
|
||||
for i, name in enumerate(["a3", "a2", "a1", "a0"]):
|
||||
records.append({"quantity": name, "value": f"{coeffs[i]:.6g}", "unit": ""})
|
||||
|
||||
elif function == "power":
|
||||
# b = c * a^n → log(b) = log(c) + n*log(a)
|
||||
valid = (a > 0) & (b > 0)
|
||||
if valid.sum() < 2:
|
||||
predicted = np.zeros_like(a)
|
||||
records.append({"quantity": "error", "value": "insufficient positive values", "unit": ""})
|
||||
else:
|
||||
log_coeffs = np.polyfit(np.log(a[valid]), np.log(b[valid]), 1)
|
||||
n_exp = log_coeffs[0]
|
||||
c = np.exp(log_coeffs[1])
|
||||
predicted = np.where(a > 0, c * np.power(a, n_exp), 0)
|
||||
records.append({"quantity": "exponent", "value": f"{n_exp:.6g}", "unit": ""})
|
||||
records.append({"quantity": "coefficient", "value": f"{c:.6g}", "unit": ""})
|
||||
|
||||
elif function == "logarithmic":
|
||||
# b = a0 + a1 * log(a)
|
||||
valid = a > 0
|
||||
if valid.sum() < 2:
|
||||
predicted = np.zeros_like(a)
|
||||
records.append({"quantity": "error", "value": "insufficient positive values", "unit": ""})
|
||||
else:
|
||||
coeffs = np.polyfit(np.log(a[valid]), b[valid], 1)
|
||||
predicted = np.where(a > 0, np.polyval(coeffs, np.log(a)), 0)
|
||||
records.append({"quantity": "log_coeff", "value": f"{coeffs[0]:.6g}", "unit": ""})
|
||||
records.append({"quantity": "intercept", "value": f"{coeffs[1]:.6g}", "unit": ""})
|
||||
else:
|
||||
predicted = np.zeros_like(a)
|
||||
|
||||
# R² statistic
|
||||
ss_res = np.sum((b - predicted)**2)
|
||||
ss_tot = np.sum((b - b.mean())**2)
|
||||
r2 = 1.0 - ss_res / max(ss_tot, 1e-30)
|
||||
records.append({"quantity": "R²", "value": f"{r2:.6f}", "unit": ""})
|
||||
|
||||
pred_field = field_b.replace(data=predicted.reshape(field_b.data.shape))
|
||||
return (pred_field, records)
|
||||
Reference in New Issue
Block a user