diff --git a/.github/skills/flow-curve-analysis/SKILL.md b/.github/skills/flow-curve-analysis/SKILL.md index 42b717c..2946226 100644 --- a/.github/skills/flow-curve-analysis/SKILL.md +++ b/.github/skills/flow-curve-analysis/SKILL.md @@ -795,7 +795,8 @@ rheofit/ plot, list_plots, plot_info, get_plot io.py TRIOS JSON reading, column standardisation, test-type detection, URL download, rheodata-backed demo - models/ one module per model + _fitcore.py (the shared fitting engine) + models/ one thin adapter per model + _fitcore.py (the shared fitting engine); + equations, parameters, bounds and citations come from rheomodel visualization/ one module per measurement type + _plotcore.py (shared plot primitives) report.py PNG scorecard, parameter summary, PPTX builder analysis.py analyze() — load → fit → summarise → save artifacts (flow curves only) @@ -837,21 +838,26 @@ See the **Fitting Contract** at the top of this file — that section is the aut ## Adding a New Model -All fitting logic lives in `rheofit/models/_fitcore.py`. A model module supplies only its physics: +Model physics (equation, parameters, bounds, citations, nesting) lives in +**rheomodel** — add the model there first. The rheofit side is a thin adapter +in `rheofit/models/.py` that re-exports the physics and supplies only the +fitting hooks: ```python -MODEL_NAME, PARAMS, SCORECARD_PARAMS -LOG_PARAMS # names fitted in log10 space -BOUNDS # name -> (lo, hi) in physical units; lo > 0 for log params -PARENT # simpler nested model name, or None -PARENT_EXACT # True only if the parent is an exact reduction -_func(x, *params) +from rheomodel import get_model as _get_model +_model = _get_model("") +MODEL_NAME, PARAMS, SCORECARD_PARAMS, LOG_PARAMS, BOUNDS = ... +PARENT, PARENT_EXACT, CITATION, PARAM_INFO = ... +equation = _func = _model.equation +get_equation_latex = _model.get_equation_latex + initial_guess(x, y, eta) -> dict seed_from_parent(parent_values, x, y, eta) -> dict # if PARENT fit_model(df, effort, seed) -> robust_fit(sys.modules[__name__], ...) ``` -Register it in `rheofit/models/__init__.py`. Relative weighting, log-space search, multi-start, ladder seeding, polish, and error propagation are inherited automatically — never reimplement them in a model file. A newly registered model appears immediately in `rheofit.list_models()`, in `--model` on the CLI, and in this skill — but **add it to the model tables above in the same change**, otherwise the skill stops describing the library accurately. +All fitting logic lives in `rheofit/models/_fitcore.py`. Register the adapter in +`rheofit/models/__init__.py`. Relative weighting, log-space search, multi-start, ladder seeding, polish, and error propagation are inherited automatically — never reimplement them in a model file. A newly registered model appears immediately in `rheofit.list_models()`, in `--model` on the CLI, and in this skill — but **add it to the model tables above in the same change**, otherwise the skill stops describing the library accurately. ## Adding a New Measurement Type diff --git a/README.md b/README.md index b186325..0c0f754 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,8 @@ is worse than a slightly poorer fit with parameters that map onto the formula. ``` rheofit/ io.py TRIOS JSON reading, URL download, demo data - models/ one module per model + _fitcore.py (shared fitting engine) + models/ one thin adapter per model + _fitcore.py (shared fitting engine) + (equations, parameters, bounds, citations come from rheomodel) visualization/ flow-curve, frequency-sweep and amplitude-sweep plots report.py plots, PNG scorecard, parameter summary, PPTX analysis.py analyze() diff --git a/docs/conf.py b/docs/conf.py index a5e06a0..fcfdf3a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,7 +12,7 @@ project = "rheofit" copyright = "2026, rheopy" author = "rheopy" -release = "0.1.0" +release = "1.1.0" extensions = [ "myst_parser", diff --git a/pyproject.toml b/pyproject.toml index 2aa54ae..06fa574 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "rheofit" -version = "1.0.2" +version = "1.1.0" description = "Fit flow curves (viscosity vs shear rate) to physically-based rheological models and quantify material properties." readme = "README.md" requires-python = ">=3.10" @@ -25,6 +25,7 @@ dependencies = [ "matplotlib>=3.6,<4", "openpyxl>=3.1.5", "rheopy-rheodata>=0.1", + "rheopy-rheomodel>=0.1", ] [project.urls] diff --git a/rheofit/__init__.py b/rheofit/__init__.py index f56ffbe..1fc04e0 100644 --- a/rheofit/__init__.py +++ b/rheofit/__init__.py @@ -29,7 +29,7 @@ from .models import MODELS from .visualization import PLOTS, get_plot, list_plots, plot, plot_info -__version__ = "1.0.2" +__version__ = "1.1.0" __all__ = [ "Analysis", diff --git a/rheofit/models/bingham.py b/rheofit/models/bingham.py index c6f1919..b82c0f8 100644 --- a/rheofit/models/bingham.py +++ b/rheofit/models/bingham.py @@ -4,6 +4,8 @@ """ import numpy as np +from rheomodel import get_model as _get_model + from ._fitcore import DEFAULT_EFFORT, est_power_law, est_sigma_y, robust_fit MODEL_NAME = "bingham" @@ -11,21 +13,25 @@ SCORECARD_PARAMS = ["sigma_y", "K"] LOG_PARAMS = ("sigma_y", "K") -BOUNDS = { - "sigma_y": (1e-12, np.inf), - "K": (1e-12, np.inf), -} -PARENT = None - - -def _func(x, sigma_y, K): - return sigma_y + K * x +_model = _get_model("bingham") +# --- model physics (equations, parameters, bounds, citations): rheomodel --- +MODEL_NAME = _model.MODEL_NAME +PARAMS = _model.PARAMS +SCORECARD_PARAMS = _model.SCORECARD_PARAMS +LOG_PARAMS = _model.LOG_PARAMS +BOUNDS = _model.BOUNDS +PARENT = _model.PARENT +PARENT_EXACT = getattr(_model, "PARENT_EXACT", False) +CITATION = _model.CITATION +PARAM_INFO = _model.PARAM_INFO -def get_equation_latex() -> str: - return "σ = σ_y + K·γ̇" +equation = _model.equation +_func = equation # the name _fitcore calls +get_equation_latex = _model.get_equation_latex +# --- fitting machinery (stays in rheofit) --- def initial_guess(x, y, eta) -> dict: sigma_y = est_sigma_y(x, y) residual_stress = np.maximum(y - 0.9 * sigma_y, y.max() * 1e-6) diff --git a/rheofit/models/carreau.py b/rheofit/models/carreau.py index 66f3329..aa1943f 100644 --- a/rheofit/models/carreau.py +++ b/rheofit/models/carreau.py @@ -4,30 +4,29 @@ """ import numpy as np +from rheomodel import get_model as _get_model + from ._fitcore import (DEFAULT_EFFORT, est_eta_0, est_lambda, est_power_law, robust_fit) +_model = _get_model("carreau") -MODEL_NAME = "carreau" -PARAMS = ["eta_0", "lambda_val", "n"] -SCORECARD_PARAMS = ["eta_0"] - -LOG_PARAMS = ("eta_0", "lambda_val") -BOUNDS = { - "eta_0": (1e-12, np.inf), - "lambda_val": (1e-12, np.inf), - "n": (0.01, 1.0), -} -PARENT = None - - -def _func(x, eta_0, lambda_val, n): - return eta_0 * x * (1.0 + (lambda_val * x) ** 2) ** ((n - 1.0) / 2.0) - +# --- model physics (equations, parameters, bounds, citations): rheomodel --- +MODEL_NAME = _model.MODEL_NAME +PARAMS = _model.PARAMS +SCORECARD_PARAMS = _model.SCORECARD_PARAMS +LOG_PARAMS = _model.LOG_PARAMS +BOUNDS = _model.BOUNDS +PARENT = _model.PARENT +PARENT_EXACT = getattr(_model, "PARENT_EXACT", False) +CITATION = _model.CITATION +PARAM_INFO = _model.PARAM_INFO -def get_equation_latex() -> str: - return "σ = η₀·γ̇·[1+(λ·γ̇)²]^((n-1)/2)" +equation = _model.equation +_func = equation # the name _fitcore calls +get_equation_latex = _model.get_equation_latex +# --- fitting machinery (stays in rheofit) --- def initial_guess(x, y, eta) -> dict: # the high-rate power-law slope of stress is the Carreau exponent n _, n_hi = est_power_law(x, y) diff --git a/rheofit/models/carreau_carreau.py b/rheofit/models/carreau_carreau.py index 14344c7..cedb89a 100644 --- a/rheofit/models/carreau_carreau.py +++ b/rheofit/models/carreau_carreau.py @@ -2,7 +2,8 @@ Carreau-Carreau — two shear-thinning components, no yield stress σ = η₀,₁·γ̇·[1+(λ₁·γ̇)²]^(-¼) + η₀,₂·γ̇·[1+(λ₂·γ̇)²]^(-½) """ -import numpy as np + +from rheomodel import get_model as _get_model from ._fitcore import DEFAULT_EFFORT, est_eta_0, est_lambda, robust_fit @@ -11,27 +12,25 @@ SCORECARD_PARAMS = ["eta_0_1", "eta_0_2"] LOG_PARAMS = ("eta_0_1", "lambda_val_1", "eta_0_2", "lambda_val_2") -BOUNDS = { - "eta_0_1": (1e-12, np.inf), - "lambda_val_1": (1e-12, np.inf), - "eta_0_2": (1e-12, np.inf), - "lambda_val_2": (1e-12, np.inf), -} -# advisory seed only: the fixed exponents mean this is not an exact reduction -PARENT = "carreau" -PARENT_EXACT = False - - -def _func(x, eta_0_1, lambda_val_1, eta_0_2, lambda_val_2): - c1 = eta_0_1 * x * (1.0 + (lambda_val_1 * x) ** 2) ** (-0.25) - c2 = eta_0_2 * x * (1.0 + (lambda_val_2 * x) ** 2) ** (-0.5) - return c1 + c2 +_model = _get_model("carreau_carreau") +# --- model physics (equations, parameters, bounds, citations): rheomodel --- +MODEL_NAME = _model.MODEL_NAME +PARAMS = _model.PARAMS +SCORECARD_PARAMS = _model.SCORECARD_PARAMS +LOG_PARAMS = _model.LOG_PARAMS +BOUNDS = _model.BOUNDS +PARENT = _model.PARENT +PARENT_EXACT = getattr(_model, "PARENT_EXACT", False) +CITATION = _model.CITATION +PARAM_INFO = _model.PARAM_INFO -def get_equation_latex() -> str: - return r"$\sigma = \eta_{0,1} \dot{\gamma} \left[1+(\lambda_1 \dot{\gamma})^2\right]^{-1/4} + \eta_{0,2} \dot{\gamma} \left[1+(\lambda_2 \dot{\gamma})^2\right]^{-1/2}$" +equation = _model.equation +_func = equation # the name _fitcore calls +get_equation_latex = _model.get_equation_latex +# --- fitting machinery (stays in rheofit) --- def initial_guess(x, y, eta) -> dict: eta_0 = est_eta_0(x, eta) lam = est_lambda(x, eta) diff --git a/rheofit/models/casson.py b/rheofit/models/casson.py index 0be8361..07b16c6 100644 --- a/rheofit/models/casson.py +++ b/rheofit/models/casson.py @@ -4,6 +4,8 @@ """ import numpy as np +from rheomodel import get_model as _get_model + from ._fitcore import DEFAULT_EFFORT, est_sigma_y, robust_fit MODEL_NAME = "casson" @@ -11,21 +13,25 @@ SCORECARD_PARAMS = ["sigma_y", "K"] LOG_PARAMS = ("sigma_y", "K") -BOUNDS = { - "sigma_y": (1e-12, np.inf), - "K": (1e-12, np.inf), -} -PARENT = None - - -def _func(x, sigma_y, K): - return (np.sqrt(sigma_y) + np.sqrt(K * x)) ** 2 +_model = _get_model("casson") +# --- model physics (equations, parameters, bounds, citations): rheomodel --- +MODEL_NAME = _model.MODEL_NAME +PARAMS = _model.PARAMS +SCORECARD_PARAMS = _model.SCORECARD_PARAMS +LOG_PARAMS = _model.LOG_PARAMS +BOUNDS = _model.BOUNDS +PARENT = _model.PARENT +PARENT_EXACT = getattr(_model, "PARENT_EXACT", False) +CITATION = _model.CITATION +PARAM_INFO = _model.PARAM_INFO -def get_equation_latex() -> str: - return "σ = (\\sqrt{\\sigma_y} + \\sqrt{K\\cdot\\dot\\gamma})^2" +equation = _model.equation +_func = equation # the name _fitcore calls +get_equation_latex = _model.get_equation_latex +# --- fitting machinery (stays in rheofit) --- def initial_guess(x, y, eta) -> dict: sigma_y = est_sigma_y(x, y) sqrt_y = np.sqrt(np.maximum(y, 1e-12)) diff --git a/rheofit/models/herschel_bulkley.py b/rheofit/models/herschel_bulkley.py index 8d0855d..571d2d6 100644 --- a/rheofit/models/herschel_bulkley.py +++ b/rheofit/models/herschel_bulkley.py @@ -4,6 +4,8 @@ """ import numpy as np +from rheomodel import get_model as _get_model + from ._fitcore import DEFAULT_EFFORT, est_power_law, est_sigma_y, robust_fit MODEL_NAME = "herschel_bulkley" @@ -11,24 +13,25 @@ SCORECARD_PARAMS = ["sigma_y", "K", "n"] LOG_PARAMS = ("sigma_y", "K") -BOUNDS = { - "sigma_y": (1e-12, np.inf), - "K": (1e-12, np.inf), - "n": (0.01, 2.0), -} -# exact reduction: n -> 1 recovers Bingham -PARENT = "bingham" -PARENT_EXACT = True - - -def _func(x, sigma_y, K, n): - return sigma_y + K * x ** n +_model = _get_model("herschel_bulkley") +# --- model physics (equations, parameters, bounds, citations): rheomodel --- +MODEL_NAME = _model.MODEL_NAME +PARAMS = _model.PARAMS +SCORECARD_PARAMS = _model.SCORECARD_PARAMS +LOG_PARAMS = _model.LOG_PARAMS +BOUNDS = _model.BOUNDS +PARENT = _model.PARENT +PARENT_EXACT = getattr(_model, "PARENT_EXACT", False) +CITATION = _model.CITATION +PARAM_INFO = _model.PARAM_INFO -def get_equation_latex() -> str: - return "σ = σ_y + K·γ̇ⁿ" +equation = _model.equation +_func = equation # the name _fitcore calls +get_equation_latex = _model.get_equation_latex +# --- fitting machinery (stays in rheofit) --- def initial_guess(x, y, eta) -> dict: sigma_y = est_sigma_y(x, y) # strip the plateau before reading the power-law slope diff --git a/rheofit/models/power_law.py b/rheofit/models/power_law.py index f5a6eb0..48d1809 100644 --- a/rheofit/models/power_law.py +++ b/rheofit/models/power_law.py @@ -2,7 +2,8 @@ Power Law — simple shear-thinning, no yield stress σ = K·γ̇ⁿ """ -import numpy as np + +from rheomodel import get_model as _get_model from ._fitcore import DEFAULT_EFFORT, est_power_law, robust_fit @@ -11,21 +12,25 @@ SCORECARD_PARAMS = ["K", "n"] LOG_PARAMS = ("K",) -BOUNDS = { - "K": (1e-12, np.inf), - "n": (0.01, 2.0), -} -PARENT = None - - -def _func(x, K, n): - return K * x ** n +_model = _get_model("power_law") +# --- model physics (equations, parameters, bounds, citations): rheomodel --- +MODEL_NAME = _model.MODEL_NAME +PARAMS = _model.PARAMS +SCORECARD_PARAMS = _model.SCORECARD_PARAMS +LOG_PARAMS = _model.LOG_PARAMS +BOUNDS = _model.BOUNDS +PARENT = _model.PARENT +PARENT_EXACT = getattr(_model, "PARENT_EXACT", False) +CITATION = _model.CITATION +PARAM_INFO = _model.PARAM_INFO -def get_equation_latex() -> str: - return "σ = K·γ̇ⁿ" +equation = _model.equation +_func = equation # the name _fitcore calls +get_equation_latex = _model.get_equation_latex +# --- fitting machinery (stays in rheofit) --- def initial_guess(x, y, eta) -> dict: K, n = est_power_law(x, y) return {"K": K, "n": n} diff --git a/rheofit/models/tc.py b/rheofit/models/tc.py index 8859e1c..118d025 100644 --- a/rheofit/models/tc.py +++ b/rheofit/models/tc.py @@ -2,32 +2,30 @@ TC — Two-Component (yield stress + Newtonian background) σ = σ_y + σ_y·(γ̇/γ̇_c)^½ + η_bg·γ̇ """ -import numpy as np + +from rheomodel import get_model as _get_model from ._fitcore import (DEFAULT_EFFORT, est_eta_bg, est_gamma_dot_c, est_sigma_y, robust_fit) +_model = _get_model("tc") -MODEL_NAME = "tc" -PARAMS = ["sigma_y", "gamma_dot_c", "eta_bg"] -SCORECARD_PARAMS = ["sigma_y", "eta_bg"] - -LOG_PARAMS = ("sigma_y", "gamma_dot_c", "eta_bg") -BOUNDS = { - "sigma_y": (1e-12, np.inf), - "gamma_dot_c": (1e-6, np.inf), - "eta_bg": (1e-12, np.inf), -} -PARENT = None - - -def _func(x, sigma_y, gamma_dot_c, eta_bg): - return sigma_y + sigma_y * np.sqrt(x / gamma_dot_c) + eta_bg * x - +# --- model physics (equations, parameters, bounds, citations): rheomodel --- +MODEL_NAME = _model.MODEL_NAME +PARAMS = _model.PARAMS +SCORECARD_PARAMS = _model.SCORECARD_PARAMS +LOG_PARAMS = _model.LOG_PARAMS +BOUNDS = _model.BOUNDS +PARENT = _model.PARENT +PARENT_EXACT = getattr(_model, "PARENT_EXACT", False) +CITATION = _model.CITATION +PARAM_INFO = _model.PARAM_INFO -def get_equation_latex() -> str: - return "σ = σ_y + σ_y·(γ̇/γ̇_c)^½ + η_bg·γ̇" +equation = _model.equation +_func = equation # the name _fitcore calls +get_equation_latex = _model.get_equation_latex +# --- fitting machinery (stays in rheofit) --- def initial_guess(x, y, eta) -> dict: return { "sigma_y": est_sigma_y(x, y), diff --git a/rheofit/models/tc_carreau.py b/rheofit/models/tc_carreau.py index 98473c8..038bc4f 100644 --- a/rheofit/models/tc_carreau.py +++ b/rheofit/models/tc_carreau.py @@ -2,37 +2,30 @@ TC-Carreau — yield stress + single Carreau shear-thinning σ = σ_y + σ_y·(γ̇/γ̇_c)^½ + η₀·γ̇·[1+(λ·γ̇)²]^(-½) """ -import numpy as np + +from rheomodel import get_model as _get_model from ._fitcore import (DEFAULT_EFFORT, est_eta_0, est_gamma_dot_c, est_lambda, est_sigma_y, robust_fit) +_model = _get_model("tc_carreau") -MODEL_NAME = "tc_carreau" -PARAMS = ["sigma_y", "gamma_dot_c", "eta_0", "lambda_val"] -SCORECARD_PARAMS = ["sigma_y", "eta_0"] - -LOG_PARAMS = ("sigma_y", "gamma_dot_c", "eta_0", "lambda_val") -BOUNDS = { - "sigma_y": (1e-12, np.inf), - "gamma_dot_c": (1e-6, np.inf), - "eta_0": (1e-12, np.inf), - "lambda_val": (1e-12, np.inf), -} -# exact reduction: lambda -> 0 turns the Carreau term into eta_bg * gamma_dot -PARENT = "tc" -PARENT_EXACT = True - - -def _func(x, sigma_y, gamma_dot_c, eta_0, lambda_val): - tc = sigma_y + sigma_y * np.sqrt(x / gamma_dot_c) - carreau = eta_0 * x * (1.0 + (lambda_val * x) ** 2) ** (-0.5) - return tc + carreau - +# --- model physics (equations, parameters, bounds, citations): rheomodel --- +MODEL_NAME = _model.MODEL_NAME +PARAMS = _model.PARAMS +SCORECARD_PARAMS = _model.SCORECARD_PARAMS +LOG_PARAMS = _model.LOG_PARAMS +BOUNDS = _model.BOUNDS +PARENT = _model.PARENT +PARENT_EXACT = getattr(_model, "PARENT_EXACT", False) +CITATION = _model.CITATION +PARAM_INFO = _model.PARAM_INFO -def get_equation_latex() -> str: - return "σ = σ_y + σ_y·(γ̇/γ̇_c)^½ + η₀·γ̇·[1+(λ·γ̇)²]^(-½)" +equation = _model.equation +_func = equation # the name _fitcore calls +get_equation_latex = _model.get_equation_latex +# --- fitting machinery (stays in rheofit) --- def initial_guess(x, y, eta) -> dict: return { "sigma_y": est_sigma_y(x, y), diff --git a/rheofit/models/tccc.py b/rheofit/models/tccc.py index 97f2c9c..1f814d4 100644 --- a/rheofit/models/tccc.py +++ b/rheofit/models/tccc.py @@ -4,41 +4,30 @@ Fit backend: scipy.optimize.least_squares (no lmfit, no tadatakit). """ -import numpy as np + +from rheomodel import get_model as _get_model from ._fitcore import (DEFAULT_EFFORT, est_eta_0, est_gamma_dot_c, est_lambda, est_sigma_y, robust_fit) +_model = _get_model("tccc") -MODEL_NAME = "tccc" -PARAMS = ["sigma_y", "gamma_dot_c", "eta_0_1", "lambda_val_1", "eta_0_2", "lambda_val_2"] -SCORECARD_PARAMS = ["sigma_y", "eta_0_1", "eta_0_2"] - -LOG_PARAMS = ("sigma_y", "gamma_dot_c", "eta_0_1", "lambda_val_1", - "eta_0_2", "lambda_val_2") -BOUNDS = { - "sigma_y": (1e-12, np.inf), - "gamma_dot_c": (1e-6, np.inf), - "eta_0_1": (1e-12, np.inf), - "lambda_val_1": (1e-12, np.inf), - "eta_0_2": (1e-12, np.inf), - "lambda_val_2": (1e-12, np.inf), -} -# exact reduction: eta_0_1 -> 0 recovers TC-Carreau -PARENT = "tc_carreau" -PARENT_EXACT = True - - -def _func(x, sigma_y, gamma_dot_c, eta_0_1, lambda_val_1, eta_0_2, lambda_val_2): - tc = sigma_y + sigma_y * np.sqrt(x / gamma_dot_c) - c1 = eta_0_1 * x * (1.0 + (lambda_val_1 * x) ** 2) ** (-0.25) - c2 = eta_0_2 * x * (1.0 + (lambda_val_2 * x) ** 2) ** (-0.5) - return tc + c1 + c2 - +# --- model physics (equations, parameters, bounds, citations): rheomodel --- +MODEL_NAME = _model.MODEL_NAME +PARAMS = _model.PARAMS +SCORECARD_PARAMS = _model.SCORECARD_PARAMS +LOG_PARAMS = _model.LOG_PARAMS +BOUNDS = _model.BOUNDS +PARENT = _model.PARENT +PARENT_EXACT = getattr(_model, "PARENT_EXACT", False) +CITATION = _model.CITATION +PARAM_INFO = _model.PARAM_INFO -def get_equation_latex() -> str: - return "σ = σ_y + σ_y·(γ̇/γ̇_c)^½ + η₀,₁·γ̇·[1+(λ₁·γ̇)²]^(-¼) + η₀,₂·γ̇·[1+(λ₂·γ̇)²]^(-½)" +equation = _model.equation +_func = equation # the name _fitcore calls +get_equation_latex = _model.get_equation_latex +# --- fitting machinery (stays in rheofit) --- def initial_guess(x, y, eta) -> dict: eta_0 = est_eta_0(x, eta) lam = est_lambda(x, eta) diff --git a/rheofit/skills/flow-curve-analysis/SKILL.md b/rheofit/skills/flow-curve-analysis/SKILL.md index 42b717c..2946226 100644 --- a/rheofit/skills/flow-curve-analysis/SKILL.md +++ b/rheofit/skills/flow-curve-analysis/SKILL.md @@ -795,7 +795,8 @@ rheofit/ plot, list_plots, plot_info, get_plot io.py TRIOS JSON reading, column standardisation, test-type detection, URL download, rheodata-backed demo - models/ one module per model + _fitcore.py (the shared fitting engine) + models/ one thin adapter per model + _fitcore.py (the shared fitting engine); + equations, parameters, bounds and citations come from rheomodel visualization/ one module per measurement type + _plotcore.py (shared plot primitives) report.py PNG scorecard, parameter summary, PPTX builder analysis.py analyze() — load → fit → summarise → save artifacts (flow curves only) @@ -837,21 +838,26 @@ See the **Fitting Contract** at the top of this file — that section is the aut ## Adding a New Model -All fitting logic lives in `rheofit/models/_fitcore.py`. A model module supplies only its physics: +Model physics (equation, parameters, bounds, citations, nesting) lives in +**rheomodel** — add the model there first. The rheofit side is a thin adapter +in `rheofit/models/.py` that re-exports the physics and supplies only the +fitting hooks: ```python -MODEL_NAME, PARAMS, SCORECARD_PARAMS -LOG_PARAMS # names fitted in log10 space -BOUNDS # name -> (lo, hi) in physical units; lo > 0 for log params -PARENT # simpler nested model name, or None -PARENT_EXACT # True only if the parent is an exact reduction -_func(x, *params) +from rheomodel import get_model as _get_model +_model = _get_model("") +MODEL_NAME, PARAMS, SCORECARD_PARAMS, LOG_PARAMS, BOUNDS = ... +PARENT, PARENT_EXACT, CITATION, PARAM_INFO = ... +equation = _func = _model.equation +get_equation_latex = _model.get_equation_latex + initial_guess(x, y, eta) -> dict seed_from_parent(parent_values, x, y, eta) -> dict # if PARENT fit_model(df, effort, seed) -> robust_fit(sys.modules[__name__], ...) ``` -Register it in `rheofit/models/__init__.py`. Relative weighting, log-space search, multi-start, ladder seeding, polish, and error propagation are inherited automatically — never reimplement them in a model file. A newly registered model appears immediately in `rheofit.list_models()`, in `--model` on the CLI, and in this skill — but **add it to the model tables above in the same change**, otherwise the skill stops describing the library accurately. +All fitting logic lives in `rheofit/models/_fitcore.py`. Register the adapter in +`rheofit/models/__init__.py`. Relative weighting, log-space search, multi-start, ladder seeding, polish, and error propagation are inherited automatically — never reimplement them in a model file. A newly registered model appears immediately in `rheofit.list_models()`, in `--model` on the CLI, and in this skill — but **add it to the model tables above in the same change**, otherwise the skill stops describing the library accurately. ## Adding a New Measurement Type