Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions .github/skills/flow-curve-analysis/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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/<name>.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("<name>")
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

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
project = "rheofit"
copyright = "2026, rheopy"
author = "rheopy"
release = "0.1.0"
release = "1.1.0"

extensions = [
"myst_parser",
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -25,6 +25,7 @@ dependencies = [
"matplotlib>=3.6,<4",
"openpyxl>=3.1.5",
"rheopy-rheodata>=0.1",
"rheopy-rheomodel>=0.1",
]

[project.urls]
Expand Down
2 changes: 1 addition & 1 deletion rheofit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
28 changes: 17 additions & 11 deletions rheofit/models/bingham.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,34 @@
"""
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"
PARAMS = ["sigma_y", "K"]
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)
Expand Down
35 changes: 17 additions & 18 deletions rheofit/models/carreau.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
35 changes: 17 additions & 18 deletions rheofit/models/carreau_carreau.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down
28 changes: 17 additions & 11 deletions rheofit/models/casson.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,34 @@
"""
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"
PARAMS = ["sigma_y", "K"]
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))
Expand Down
31 changes: 17 additions & 14 deletions rheofit/models/herschel_bulkley.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,34 @@
"""
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"
PARAMS = ["sigma_y", "K", "n"]
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
Expand Down
29 changes: 17 additions & 12 deletions rheofit/models/power_law.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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}
Expand Down
Loading
Loading