Source code for streamlit_app.sections.fitting
"""Fitting logic and equation selection UI for the Streamlit app."""
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
import streamlit as st
from config import PLOT_CONFIG
from i18n import t
from utils import get_logger
from streamlit_app.sections.data import get_temp_output_dir, get_variable_names
logger = get_logger(__name__)
[docs]
def perform_fit(
data: Any,
x_name: str,
y_name: str,
equation_name: str,
plot_name: str,
custom_formula: Optional[str] = None,
parameter_names: Optional[List[str]] = None,
show_title: Optional[bool] = None,
) -> Optional[Dict[str, Any]]:
"""
Perform curve fitting for the given dataset and equation selection.
This helper wraps the core fitting logic used by the Streamlit UI:
it resolves either a predefined fitting function or a custom
:class:`~fitting.custom_function_evaluator.CustomFunctionEvaluator`,
computes the fit and generates the corresponding plot.
Args:
data: Dataset with columns ``x_name``, ``y_name`` and optionally
their uncertainties ``u{x_name}``, ``u{y_name}``.
x_name: Name of the independent variable column.
y_name: Name of the dependent variable column.
equation_name: Internal equation identifier or ``'custom_formula'``.
plot_name: Base name used for the generated plot.
custom_formula: Custom formula string when ``equation_name`` is
``'custom_formula'``.
parameter_names: List of parameter names used in ``custom_formula``.
show_title: If set, override plot title visibility (from env) for this
fit. ``None`` uses the value from ``PLOT_SHOW_TITLE``.
Returns:
Dictionary with keys ``equation_name``, ``parameters``, ``equation``,
``plot_path`` and ``plot_name`` when the fit succeeds, or ``None``
if the operation fails or is not supported.
"""
from fitting import CustomFunctionEvaluator, get_fitting_function
from plotting import create_plot
from utils import FittingError
from config import FILE_CONFIG
try:
if equation_name == "custom_formula" and custom_formula and parameter_names:
evaluator = CustomFunctionEvaluator(custom_formula, parameter_names)
fit_function = evaluator.fit
else:
fit_function = get_fitting_function(equation_name)
if fit_function is None:
st.error(t("error.fitting_error"))
return None
result_fit = fit_function(data, x_name, y_name)
text, y_fitted, equation = result_fit[0], result_fit[1], result_fit[2]
fit_info = result_fit[3] if len(result_fit) >= 4 else None
x = data[x_name]
y = data[y_name]
ux_col = f"u{x_name}"
uy_col = f"u{y_name}"
ux = data[ux_col] if ux_col in data.columns else [0.0] * len(x)
uy = data[uy_col] if uy_col in data.columns else [0.0] * len(y)
display_name = equation_name.replace("_", " ").title()
st.session_state.plot_counter += 1
filename = f"{plot_name}_{st.session_state.plot_counter}"
plot_ext = FILE_CONFIG.get("plot_format", "png")
out_path = get_temp_output_dir() / f"fit_{filename}.{plot_ext}"
plot_config = (
{**PLOT_CONFIG, "show_title": show_title}
if show_title is not None
else None
)
output_path = create_plot(
x,
y,
ux,
uy,
y_fitted,
plot_name,
x_name,
y_name,
output_path=str(out_path),
fit_info=fit_info,
plot_config=plot_config,
)
result: Dict[str, Any] = {
"equation_name": display_name,
"parameters": text,
"equation": equation,
"plot_path": output_path,
"plot_name": plot_name,
}
# When output is PDF, plotting creates a _preview.png for display; use it for in-app visualization
if Path(output_path).suffix.lower() == ".pdf":
preview = Path(output_path).parent / (
Path(output_path).stem + "_preview.png"
)
if preview.exists():
result["plot_path_display"] = str(preview)
return result
except FittingError as e:
st.error(t("error.fitting_failed_details", error=str(e)))
return None
except Exception as e:
st.error(
t("error.fitting_failed_generic", error_type=type(e).__name__, error=str(e))
)
logger.error(f"Fitting error: {str(e)}", exc_info=True)
return None
def _create_equation_options(equation_types: List[str]) -> Dict[str, str]:
"""
Build a mapping from translated equation labels to internal keys.
Args:
equation_types: List of internal equation identifiers.
Returns:
Dictionary mapping humanâreadable labels to equation keys,
including a ``'custom_formula'`` entry.
"""
equation_options: Dict[str, str] = {}
for eq in equation_types:
key = f"equations.{eq}"
eq_name = t(key)
if eq_name == key:
eq_name = eq.replace("_", " ").title()
equation_options[eq_name] = eq
equation_options[t("equations.custom_formula")] = "custom_formula"
return equation_options
[docs]
def select_variables(data: Any, key_prefix: str = "") -> Tuple[str, str, str]:
"""
Show variable selection widgets and return the chosen variables and plot name.
Args:
data: Dataset from which variables are extracted.
key_prefix: Optional prefix for Streamlit widget keys to avoid clashes.
Returns:
Tuple ``(x_name, y_name, plot_name)`` selected by the user.
"""
variables = get_variable_names(data, filter_uncertainty=True)
x_default_idx = 0 if len(variables) > 0 else None
y_default_idx = 1 if len(variables) > 1 else (0 if len(variables) > 0 else None)
x_name = st.selectbox(
t("dialog.independent_variable"),
variables,
index=x_default_idx,
key=f"{key_prefix}x" if key_prefix else None,
)
y_name = st.selectbox(
t("dialog.dependent_variable"),
variables,
index=y_default_idx,
key=f"{key_prefix}y" if key_prefix else None,
)
plot_name = st.text_input(
t("dialog.plot_name"),
value="fit",
key=f"{key_prefix}plot" if key_prefix else None,
)
return x_name, y_name, plot_name
[docs]
def show_equation_selector(
equation_types: List[str],
) -> Tuple[str, Optional[str], Optional[List[str]]]:
"""
Show equation type selector in the sidebar/content area.
Args:
equation_types: List of available equation identifiers.
Returns:
Tuple ``(equation_key, custom_formula, parameter_names)`` where
``custom_formula`` and ``parameter_names`` are only populated
when the customâformula option is selected.
"""
from config import EQUATIONS
equation_options = _create_equation_options(equation_types)
selected_label = st.selectbox(
t("dialog.select_equation"),
options=list(equation_options.keys()),
key="equation_selector",
)
selected_equation = equation_options[selected_label]
if selected_equation != "custom_formula":
desc_key = f"equations_descriptions.{selected_equation}"
desc = t(desc_key)
if desc == desc_key:
desc = selected_equation.replace("_", " ").title()
formula = EQUATIONS.get(selected_equation, {}).get("formula", "")
st.caption(f"**{desc}** : {formula}")
custom_formula = None
parameter_names = None
if selected_equation == "custom_formula":
st.info(t("dialog.formula_example"))
num_params = st.number_input(
t("dialog.num_parameters"), min_value=1, max_value=10, value=2, step=1
)
parameter_names = []
cols = st.columns(min(int(num_params), 3))
for i in range(int(num_params)):
col_idx = i % len(cols)
with cols[col_idx]:
param_name = st.text_input(
t("dialog.parameter_name", index=i + 1),
value=f"p{i + 1}",
key=f"param_{i}",
)
parameter_names.append(param_name)
custom_formula = st.text_input(
t("dialog.custom_formula_prompt"), placeholder="a*t**2 + b*t + c"
)
return selected_equation, custom_formula, parameter_names
def _show_plot_title_checkbox(key_prefix: str = "") -> bool:
"""
Show checkbox to toggle plot title visibility for the next fit.
Default value comes from PLOT_CONFIG (env PLOT_SHOW_TITLE).
Args:
key_prefix: Prefix for Streamlit widget key to avoid clashes.
Returns:
Whether to show the plot title (True) or not (False).
"""
return st.checkbox(
t("config.label_PLOT_SHOW_TITLE"),
value=PLOT_CONFIG.get("show_title", False),
key=f"{key_prefix}show_title" if key_prefix else "show_title",
help=t("config.desc_PLOT_SHOW_TITLE"),
)