rheofit 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rheofit/__init__.py +54 -0
- rheofit/__main__.py +5 -0
- rheofit/analysis.py +224 -0
- rheofit/cli.py +100 -0
- rheofit/data/linear_polymer_HA_solution.json +7402 -0
- rheofit/data/structured_shampoo.json +7169 -0
- rheofit/io.py +268 -0
- rheofit/models/__init__.py +19 -0
- rheofit/models/_fitcore.py +351 -0
- rheofit/models/carreau.py +43 -0
- rheofit/models/carreau_carreau.py +58 -0
- rheofit/models/herschel_bulkley.py +46 -0
- rheofit/models/power_law.py +36 -0
- rheofit/models/tc.py +41 -0
- rheofit/models/tc_carreau.py +56 -0
- rheofit/models/tccc.py +68 -0
- rheofit/report.py +451 -0
- rheofit/visualization/__init__.py +84 -0
- rheofit/visualization/_plotcore.py +284 -0
- rheofit/visualization/amplitude_sweep.py +149 -0
- rheofit/visualization/flow_curve.py +131 -0
- rheofit/visualization/frequency_sweep.py +128 -0
- rheofit-1.0.0.dist-info/METADATA +158 -0
- rheofit-1.0.0.dist-info/RECORD +27 -0
- rheofit-1.0.0.dist-info/WHEEL +4 -0
- rheofit-1.0.0.dist-info/entry_points.txt +2 -0
- rheofit-1.0.0.dist-info/licenses/LICENSE +21 -0
rheofit/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""rheofit — fit flow curves (viscosity vs shear rate) to rheological models.
|
|
2
|
+
|
|
3
|
+
A flow curve is the fingerprint of a non-Newtonian fluid. Fitting it with a
|
|
4
|
+
physically-based model quantifies material properties (yield stress, zero-shear
|
|
5
|
+
viscosity, relaxation time, thinning exponent), gives a concise description of
|
|
6
|
+
the material, and — when tied to the formulation — tells a formulator which
|
|
7
|
+
lever to move to hit a property target.
|
|
8
|
+
|
|
9
|
+
Typical use::
|
|
10
|
+
|
|
11
|
+
import rheofit
|
|
12
|
+
|
|
13
|
+
rheofit.print_steps("sample.json") # 1. discover steps
|
|
14
|
+
df = rheofit.load_step("sample.json", 0) # 2. load one step
|
|
15
|
+
res = rheofit.fit(df, "tc") # 3. fit it
|
|
16
|
+
rheofit.plot(df, fits=res) # 4. look at it
|
|
17
|
+
a = rheofit.analyze("sample.json", steps=[0, 2], # or do it all at once
|
|
18
|
+
model="tc", labels=["25C", "40C"])
|
|
19
|
+
|
|
20
|
+
Every fit minimises the *relative* residual ``(model - data) / |data|``, so
|
|
21
|
+
``RedChi2`` is dimensionless and comparable across steps, samples and models.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from .analysis import Analysis, analyze, fit, get_model, list_models, model_info, print_steps
|
|
26
|
+
from .io import (DEMO_SAMPLE_NAME, demo_source, detect_test_type, discover_steps,
|
|
27
|
+
load_step, load_steps)
|
|
28
|
+
from .models import MODELS
|
|
29
|
+
from .visualization import PLOTS, get_plot, list_plots, plot, plot_info
|
|
30
|
+
|
|
31
|
+
__version__ = "0.1.0"
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"Analysis",
|
|
35
|
+
"DEMO_SAMPLE_NAME",
|
|
36
|
+
"MODELS",
|
|
37
|
+
"PLOTS",
|
|
38
|
+
"analyze",
|
|
39
|
+
"demo_source",
|
|
40
|
+
"detect_test_type",
|
|
41
|
+
"discover_steps",
|
|
42
|
+
"fit",
|
|
43
|
+
"get_model",
|
|
44
|
+
"get_plot",
|
|
45
|
+
"list_models",
|
|
46
|
+
"list_plots",
|
|
47
|
+
"load_step",
|
|
48
|
+
"load_steps",
|
|
49
|
+
"model_info",
|
|
50
|
+
"plot",
|
|
51
|
+
"plot_info",
|
|
52
|
+
"print_steps",
|
|
53
|
+
"__version__",
|
|
54
|
+
]
|
rheofit/__main__.py
ADDED
rheofit/analysis.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""High-level analysis workflow: load steps, fit a model, save artifacts."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from . import io as trios_io
|
|
10
|
+
from . import report
|
|
11
|
+
from .models import MODELS
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def list_models() -> list[str]:
|
|
15
|
+
"""Names of the available rheological models."""
|
|
16
|
+
return sorted(MODELS)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_model(name: str):
|
|
20
|
+
"""Return the model module for ``name`` (raises for unknown models)."""
|
|
21
|
+
try:
|
|
22
|
+
return MODELS[name]
|
|
23
|
+
except KeyError:
|
|
24
|
+
raise ValueError(f"Unknown model '{name}'. Available: {list_models()}") from None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def model_info(name: str) -> dict:
|
|
28
|
+
"""Metadata of a model: equation, parameters, scorecard params, nested parent."""
|
|
29
|
+
m = get_model(name)
|
|
30
|
+
return {
|
|
31
|
+
"name": m.MODEL_NAME,
|
|
32
|
+
"equation": m.get_equation_latex(),
|
|
33
|
+
"params": list(m.PARAMS),
|
|
34
|
+
"scorecard_params": list(m.SCORECARD_PARAMS),
|
|
35
|
+
"parent": getattr(m, "PARENT", None),
|
|
36
|
+
"parent_exact": getattr(m, "PARENT_EXACT", False),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def fit(df: pd.DataFrame, model: str, effort: str = "thorough", seed: int = 0) -> dict:
|
|
41
|
+
"""Fit one flow-curve DataFrame with ``model``.
|
|
42
|
+
|
|
43
|
+
``df`` needs a ``'Shear rate / 1/s'`` and a ``'Stress / Pa'`` column
|
|
44
|
+
(``'Viscosity / Pa.s'`` is used when present, otherwise derived).
|
|
45
|
+
Returns the result dict: ``params`` (value/stderr), ``redchi``, ``cond``,
|
|
46
|
+
``x``, ``y_data``, ``y_fit``, ``notes``.
|
|
47
|
+
"""
|
|
48
|
+
return get_model(model).fit_model(df, effort=effort, seed=seed)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class Analysis:
|
|
53
|
+
"""Result of :func:`analyze` — fits, summary table and saved artifact paths."""
|
|
54
|
+
|
|
55
|
+
sample_name: str
|
|
56
|
+
model: str
|
|
57
|
+
equation: str
|
|
58
|
+
results: dict = field(default_factory=dict)
|
|
59
|
+
summary: pd.DataFrame = field(default_factory=pd.DataFrame)
|
|
60
|
+
outputs: list[str] = field(default_factory=list)
|
|
61
|
+
output_dir: Path | None = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def analyze(
|
|
65
|
+
source: str,
|
|
66
|
+
steps: list[int],
|
|
67
|
+
model: str,
|
|
68
|
+
labels: list[str] | None = None,
|
|
69
|
+
effort: str = "thorough",
|
|
70
|
+
seed: int = 0,
|
|
71
|
+
output: str = "png_csv",
|
|
72
|
+
sample_name: str | None = None,
|
|
73
|
+
results_base: str | Path | None = None,
|
|
74
|
+
verbose: bool = True,
|
|
75
|
+
) -> Analysis:
|
|
76
|
+
"""Fit selected TRIOS steps and write the requested artifacts.
|
|
77
|
+
|
|
78
|
+
``source`` is a local TRIOS JSON path or an HTTP(S) URL.
|
|
79
|
+
``output`` is ``'png_csv'`` (default), ``'pptx'``, ``'both'`` or ``'none'``
|
|
80
|
+
(fit only, write nothing).
|
|
81
|
+
"""
|
|
82
|
+
if output not in {"png_csv", "pptx", "both", "none"}:
|
|
83
|
+
raise ValueError("output must be one of 'png_csv', 'pptx', 'both', 'none'")
|
|
84
|
+
|
|
85
|
+
model_mod = get_model(model)
|
|
86
|
+
json_path, cleanup_tmp = trios_io.materialize(source)
|
|
87
|
+
|
|
88
|
+
name = (sample_name or "").strip() or json_path.stem or "remote_sample"
|
|
89
|
+
|
|
90
|
+
if results_base is not None:
|
|
91
|
+
base_dir = Path(results_base)
|
|
92
|
+
else:
|
|
93
|
+
base_dir = Path.cwd() if trios_io.is_http_url(source) else json_path.parent
|
|
94
|
+
out_dir = base_dir / "results" / name
|
|
95
|
+
|
|
96
|
+
def log(msg: str) -> None:
|
|
97
|
+
if verbose:
|
|
98
|
+
print(msg)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
if labels is None or len(labels) != len(steps):
|
|
102
|
+
labels = [f"Step {i}" for i in steps]
|
|
103
|
+
|
|
104
|
+
log(f"\n[*] Sample : {name}")
|
|
105
|
+
log(f" Model : {model_mod.MODEL_NAME}")
|
|
106
|
+
log(f" Source : {source}")
|
|
107
|
+
log(f" Steps : {steps} -> labels: {labels}")
|
|
108
|
+
log(f" Fit : relative-weighted, effort={effort}, seed={seed}")
|
|
109
|
+
log(f" Output : {output}")
|
|
110
|
+
|
|
111
|
+
step_data = trios_io.load_steps(json_path, steps)
|
|
112
|
+
|
|
113
|
+
res_dict: dict = {}
|
|
114
|
+
for idx, label in zip(steps, labels):
|
|
115
|
+
df = step_data[idx]
|
|
116
|
+
log(f"\n Fitting [{label}] ({len(df)} pts) ...")
|
|
117
|
+
result = model_mod.fit_model(df, effort=effort, seed=seed)
|
|
118
|
+
log(f" [OK] success={result['success']} RedChi2={result['redchi']:.3E}"
|
|
119
|
+
f" starts={result.get('n_starts', '?')}")
|
|
120
|
+
if result.get("parent"):
|
|
121
|
+
log(f" nested parent '{result['parent']}'"
|
|
122
|
+
f" RedChi2={result['parent_redchi']:.3E}")
|
|
123
|
+
for note in result.get("notes", []):
|
|
124
|
+
log(f" [!] {note}")
|
|
125
|
+
res_dict[label] = result
|
|
126
|
+
|
|
127
|
+
summary_df = report.build_parameter_summary(res_dict, model_mod.SCORECARD_PARAMS)
|
|
128
|
+
analysis = Analysis(
|
|
129
|
+
sample_name=name,
|
|
130
|
+
model=model_mod.MODEL_NAME,
|
|
131
|
+
equation=model_mod.get_equation_latex(),
|
|
132
|
+
results=res_dict,
|
|
133
|
+
summary=summary_df,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if output == "none":
|
|
137
|
+
return analysis
|
|
138
|
+
|
|
139
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
analysis.output_dir = out_dir
|
|
141
|
+
outputs: list[str] = []
|
|
142
|
+
model_tag = model_mod.MODEL_NAME.upper()
|
|
143
|
+
|
|
144
|
+
if output in {"png_csv", "both"}:
|
|
145
|
+
# One scorecard per step label when several flow sweeps are selected.
|
|
146
|
+
if len(res_dict) == 1:
|
|
147
|
+
only_label = next(iter(res_dict))
|
|
148
|
+
png_path = out_dir / f"{name} - {model_tag} Scorecard.png"
|
|
149
|
+
report.build_png_scorecard(
|
|
150
|
+
sample_name=name,
|
|
151
|
+
model_name=model_mod.MODEL_NAME,
|
|
152
|
+
equation=analysis.equation,
|
|
153
|
+
res_dict=res_dict,
|
|
154
|
+
summary_df=summary_df,
|
|
155
|
+
output_path=png_path,
|
|
156
|
+
title_suffix=only_label,
|
|
157
|
+
)
|
|
158
|
+
outputs.append(str(png_path))
|
|
159
|
+
else:
|
|
160
|
+
for label, single_res in res_dict.items():
|
|
161
|
+
single_res_dict = {label: single_res}
|
|
162
|
+
single_summary = report.build_parameter_summary(
|
|
163
|
+
single_res_dict, model_mod.SCORECARD_PARAMS
|
|
164
|
+
)
|
|
165
|
+
safe_label = report.safe_label_for_filename(label)
|
|
166
|
+
png_path = out_dir / (
|
|
167
|
+
f"{name} - {safe_label} - {model_tag} Scorecard.png"
|
|
168
|
+
)
|
|
169
|
+
report.build_png_scorecard(
|
|
170
|
+
sample_name=name,
|
|
171
|
+
model_name=model_mod.MODEL_NAME,
|
|
172
|
+
equation=analysis.equation,
|
|
173
|
+
res_dict=single_res_dict,
|
|
174
|
+
summary_df=single_summary,
|
|
175
|
+
output_path=png_path,
|
|
176
|
+
title_suffix=label,
|
|
177
|
+
)
|
|
178
|
+
outputs.append(str(png_path))
|
|
179
|
+
|
|
180
|
+
csv_path = out_dir / f"{name} - {model_tag} Scorecard Summary.csv"
|
|
181
|
+
summary_df.to_csv(csv_path, index=False)
|
|
182
|
+
outputs.append(str(csv_path))
|
|
183
|
+
|
|
184
|
+
if output in {"pptx", "both"}:
|
|
185
|
+
tmp_diag_paths = [
|
|
186
|
+
report._save_temp(report.plot_fit(res, title=f"{name} [{label}]"))
|
|
187
|
+
for label, res in res_dict.items()
|
|
188
|
+
]
|
|
189
|
+
sc_fig, val_tbl, err_tbl = report.plot_scorecard(res_dict, model_mod.SCORECARD_PARAMS)
|
|
190
|
+
tmp_sc_path = report._save_temp(sc_fig)
|
|
191
|
+
|
|
192
|
+
prs = report.build_pptx(
|
|
193
|
+
sample_name=name,
|
|
194
|
+
model_name=model_mod.MODEL_NAME,
|
|
195
|
+
equation=analysis.equation,
|
|
196
|
+
step_labels=list(res_dict),
|
|
197
|
+
tmp_diag_paths=tmp_diag_paths,
|
|
198
|
+
tmp_sc_path=tmp_sc_path,
|
|
199
|
+
val_tbl=val_tbl,
|
|
200
|
+
err_tbl=err_tbl,
|
|
201
|
+
)
|
|
202
|
+
pptx_path = out_dir / f"{name} - {model_tag} Scorecard.pptx"
|
|
203
|
+
prs.save(str(pptx_path))
|
|
204
|
+
outputs.append(str(pptx_path))
|
|
205
|
+
|
|
206
|
+
for f in tmp_diag_paths + [tmp_sc_path]:
|
|
207
|
+
Path(f).unlink(missing_ok=True)
|
|
208
|
+
|
|
209
|
+
analysis.outputs = outputs
|
|
210
|
+
log("")
|
|
211
|
+
for out in outputs:
|
|
212
|
+
log(f"[>] Saved: {out}")
|
|
213
|
+
log("")
|
|
214
|
+
return analysis
|
|
215
|
+
finally:
|
|
216
|
+
if cleanup_tmp:
|
|
217
|
+
json_path.unlink(missing_ok=True)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def print_steps(source: str) -> list[dict]:
|
|
221
|
+
"""Print the step table for a TRIOS file and return the step descriptors."""
|
|
222
|
+
steps = trios_io.discover_steps(source)
|
|
223
|
+
print(trios_io.format_step_table(steps, Path(str(source)).name))
|
|
224
|
+
return steps
|
rheofit/cli.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Command-line interface for rheofit (``rheofit`` / ``python -m rheofit``)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .analysis import analyze, list_models, print_steps
|
|
9
|
+
from .io import DEMO_SAMPLE_NAME, demo_source
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
13
|
+
p = argparse.ArgumentParser(
|
|
14
|
+
prog="rheofit",
|
|
15
|
+
description="Fit a TRIOS JSON flow curve and produce a scorecard "
|
|
16
|
+
"(PNG + CSV by default, optionally PowerPoint).",
|
|
17
|
+
)
|
|
18
|
+
p.add_argument(
|
|
19
|
+
"json_file",
|
|
20
|
+
nargs="?",
|
|
21
|
+
default=None,
|
|
22
|
+
help="Path or HTTP(S) URL to a TA Instruments TRIOS JSON file",
|
|
23
|
+
)
|
|
24
|
+
p.add_argument(
|
|
25
|
+
"--demo",
|
|
26
|
+
action="store_true",
|
|
27
|
+
help="Use the demo TRIOS JSON bundled with the library "
|
|
28
|
+
"(falls back to the online demo URL if the file is missing).",
|
|
29
|
+
)
|
|
30
|
+
p.add_argument(
|
|
31
|
+
"--steps", nargs="+", type=int, default=None, metavar="N",
|
|
32
|
+
help="Step indices to analyze (e.g. --steps 1 3). "
|
|
33
|
+
"Omit to list available steps and exit.",
|
|
34
|
+
)
|
|
35
|
+
p.add_argument(
|
|
36
|
+
"--model", default=None, choices=list_models(),
|
|
37
|
+
help="Rheological model to fit",
|
|
38
|
+
)
|
|
39
|
+
p.add_argument(
|
|
40
|
+
"--labels", nargs="+", default=None, metavar="LABEL",
|
|
41
|
+
help="Human-readable label per step (e.g. --labels 25C 40C). "
|
|
42
|
+
"Must match number of --steps. Defaults to 'Step N'.",
|
|
43
|
+
)
|
|
44
|
+
p.add_argument(
|
|
45
|
+
"--effort", default="thorough", choices=["fast", "normal", "thorough"],
|
|
46
|
+
help="Global-search intensity: number of multi-start seeds explored "
|
|
47
|
+
"before the tight polish (default: thorough).",
|
|
48
|
+
)
|
|
49
|
+
p.add_argument(
|
|
50
|
+
"--seed", type=int, default=0,
|
|
51
|
+
help="Random seed for the Sobol multi-start, for reproducible fits.",
|
|
52
|
+
)
|
|
53
|
+
p.add_argument(
|
|
54
|
+
"--sample-name", default=None,
|
|
55
|
+
help="Optional output sample name override (useful for URL inputs).",
|
|
56
|
+
)
|
|
57
|
+
p.add_argument(
|
|
58
|
+
"--output", default="png_csv", choices=["png_csv", "pptx", "both", "none"],
|
|
59
|
+
help="Artifact mode: png_csv (default), pptx, both, or none (fit only).",
|
|
60
|
+
)
|
|
61
|
+
return p.parse_args(argv)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main(argv: list[str] | None = None) -> int:
|
|
65
|
+
args = _parse_args(argv)
|
|
66
|
+
try:
|
|
67
|
+
if not args.demo and not args.json_file:
|
|
68
|
+
print("[!] Provide <json_file> or use --demo")
|
|
69
|
+
return 2
|
|
70
|
+
|
|
71
|
+
source = demo_source() if args.demo else args.json_file
|
|
72
|
+
sample_name = args.sample_name
|
|
73
|
+
if args.demo and not sample_name:
|
|
74
|
+
sample_name = DEMO_SAMPLE_NAME
|
|
75
|
+
|
|
76
|
+
if args.steps is None or args.model is None:
|
|
77
|
+
print_steps(source)
|
|
78
|
+
if args.steps is None:
|
|
79
|
+
print("Re-run with --steps <indices> --model <name>")
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
analyze(
|
|
83
|
+
source=source,
|
|
84
|
+
steps=args.steps,
|
|
85
|
+
model=args.model,
|
|
86
|
+
labels=args.labels,
|
|
87
|
+
effort=args.effort,
|
|
88
|
+
seed=args.seed,
|
|
89
|
+
output=args.output,
|
|
90
|
+
sample_name=sample_name,
|
|
91
|
+
results_base=Path.cwd() if args.demo else None,
|
|
92
|
+
)
|
|
93
|
+
return 0
|
|
94
|
+
except (RuntimeError, ValueError) as exc:
|
|
95
|
+
print(f"[!] {exc}")
|
|
96
|
+
return 1
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if __name__ == "__main__":
|
|
100
|
+
sys.exit(main())
|