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 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
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
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())