physplot 0.1.0.post1__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.
physplot/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .graph import Graph
2
+ from .error import Error
3
+ from .propagation import Propagation
4
+
5
+ __all__ = ["Graph", "Error", "Propagation"]
physplot/error.py ADDED
@@ -0,0 +1,31 @@
1
+ import numpy as np
2
+ from typing import Dict, Sequence, Union
3
+
4
+ NumberArray = Union[float, int, np.ndarray, Sequence[float]]
5
+
6
+ class Error:
7
+ def __init__(self, err_data: NumberArray):
8
+ self.err_data = np.array(err_data, dtype=float)
9
+ self.mean_vals = None
10
+ self.std_dev = None
11
+ self.std_err = None
12
+
13
+ def err(self, axis=-1, elementwise=False, print_std_err=False, print_mean=False, print_st_dev=False):
14
+ data = self.err_data
15
+ if elementwise:
16
+ axis_to_use = -1
17
+ else:
18
+ axis_to_use = axis
19
+
20
+ self.mean_vals = np.nanmean(data, axis=axis_to_use)
21
+ self.std_dev = np.nanstd(data, axis=axis_to_use, ddof=1)
22
+ n_points = data.shape[axis_to_use]
23
+ self.std_err = self.std_dev / np.sqrt(n_points)
24
+
25
+ if print_st_dev:
26
+ print(f"Std Dev along axis {axis_to_use}:\n{self.std_dev}")
27
+ if print_std_err:
28
+ print(f"Std Err along axis {axis_to_use}:\n{self.std_err}")
29
+ if print_mean:
30
+ print(f"Mean along axis {axis_to_use}:\n{self.mean_vals}")
31
+ return self.std_err
physplot/graph.py ADDED
@@ -0,0 +1,162 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ from scipy.optimize import curve_fit
4
+ from typing import Sequence, Union
5
+
6
+ NumberArray = Union[float, int, np.ndarray, Sequence[float]]
7
+
8
+ class Graph:
9
+ def __init__(self, axis, x_axis, y_axis, marker='o', y_err = None, x_err = None, name = None):
10
+ self.axis = axis
11
+ self.marker = marker
12
+ self.name = name
13
+ self.y_err = y_err
14
+ self.x_err = x_err
15
+ self.x_axis_mean, self.x_err_mean = self._normalize_axis(x_axis, x_err)
16
+ self.y_axis_mean, self.y_err_mean = self._normalize_axis(y_axis, y_err)
17
+ self.parameters = None
18
+
19
+ @staticmethod
20
+ def plot_details(rows, cols, x_size, y_size, text=None, fontsize=None):
21
+ fig, axs = plt.subplots(rows, cols, figsize=[x_size, y_size])
22
+ if text is not None and fontsize is not None:
23
+ fig.suptitle(text, fontsize=fontsize)
24
+ if rows == 1 and cols == 1:
25
+ axs = np.array([[axs]])
26
+ elif rows == 1:
27
+ axs = np.expand_dims(axs, axis=0)
28
+ elif cols == 1:
29
+ axs = np.expand_dims(axs, axis=1)
30
+ return fig, axs
31
+
32
+ def grapher(self, title=None, x_label=None, y_label=None, text=None, color=None, label=None,
33
+ x_tick_start=None, x_tick_end=None, x_tick_step=None, y_tick_start=None,
34
+ y_tick_end=None, y_tick_step=None, line_fit_bool=False,
35
+ curve_fit_bool=False, fit_func=None, p0=None, fit_colors=None, fit_labels=None,
36
+ minor_ticks=True):
37
+
38
+ if self.y_err is not None or self.x_err is not None:
39
+ self._handle_errors()
40
+ dots = self.axis.scatter(self.x_axis_mean, self.y_axis_mean, marker=self.marker,
41
+ color=color, label=label)
42
+
43
+ if line_fit_bool:
44
+ self._fit_line()
45
+ if curve_fit_bool:
46
+ self._fit_curve(fit_func, p0, fit_colors, fit_labels)
47
+ self._set_ticks(x_tick_start, x_tick_end, x_tick_step,
48
+ y_tick_start, y_tick_end, y_tick_step, minor_ticks)
49
+ self._finalize_plot(title, x_label, y_label, text)
50
+
51
+ return dots
52
+
53
+ def _handle_errors(self):
54
+ def maybe_collapse(err):
55
+ if err is None:
56
+ return None
57
+ err = np.array(err)
58
+ if err.ndim > 1 and not (err.ndim == 2 and err.shape[0] == 2):
59
+ return self._collapse_error(err)
60
+ return err
61
+
62
+ x_err_to_use = maybe_collapse(self.x_err_mean)
63
+ y_err_to_use = maybe_collapse(self.y_err_mean)
64
+
65
+ self.axis.errorbar(self.x_axis_mean, self.y_axis_mean, xerr=x_err_to_use, yerr=y_err_to_use, fmt='.', capsize=3,
66
+ color='red', label="Error")
67
+
68
+ def _fit_line(self):
69
+ self.parameters = np.polyfit(self.x_axis_mean, self.y_axis_mean, 1)
70
+ fit_line = np.polyval(self.parameters, self.x_axis_mean)
71
+ self.axis.plot(self.x_axis_mean, fit_line, color='grey', linestyle='dashed', label='Linear Fit')
72
+
73
+ def _fit_curve(self, fit_func, p0, fit_colors, fit_labels):
74
+ if fit_func is None:
75
+ raise ValueError("Provide fit_func for curve fitting.")
76
+ fit_funcs = fit_func if isinstance(fit_func, (list, tuple)) else [fit_func]
77
+ p0s = self._normalize_p0(fit_funcs, p0)
78
+ fit_colors = self._normalize_list(fit_colors, len(fit_funcs))
79
+ fit_labels = self._normalize_labels(fit_labels, len(fit_funcs))
80
+ x_fit = np.linspace(min(self.x_axis_mean), max(self.x_axis_mean), 1000)
81
+ self.parameters = []
82
+
83
+ for i, func in enumerate(fit_funcs):
84
+ try:
85
+ popt, _ = curve_fit(func, self.x_axis_mean, self.y_axis_mean, p0=p0s[i])
86
+ self.parameters.append(popt)
87
+ fitted_y = func(x_fit, *popt)
88
+ self.axis.plot(x_fit, fitted_y, color=fit_colors[i], linestyle='dashed', label=fit_labels[i])
89
+ except Exception as e:
90
+ print(f"Curve Fit {fit_labels[i]} failed: {e}")
91
+ self.parameters.append(None)
92
+
93
+ def _set_ticks(self, x_start, x_end, x_step, y_start, y_end, y_step, minor_ticks):
94
+ if minor_ticks:
95
+ self.axis.minorticks_on()
96
+ if all(v is not None for v in [x_start, x_end, x_step]):
97
+ self.axis.set_xticks(np.arange(x_start, x_end + x_step, x_step))
98
+ if all(v is not None for v in [y_start, y_end, y_step]):
99
+ self.axis.set_yticks(np.arange(y_start, y_end + y_step, y_step))
100
+
101
+ def _finalize_plot(self, title, x_label, y_label, text):
102
+ if title: self.axis.set_title(title, fontsize=12)
103
+ if x_label: self.axis.set_xlabel(x_label)
104
+ if y_label: self.axis.set_ylabel(y_label)
105
+ if text:
106
+ self.axis.text(0.5, 0.9, text, transform=self.axis.transAxes, fontsize=12, ha='center', color='blue')
107
+ self.axis.legend()
108
+ self.axis.grid()
109
+
110
+ @staticmethod
111
+ def _normalize_axis(axis_data, axis_err=None):
112
+ axis_data = np.array(axis_data, dtype=float)
113
+ if axis_data.ndim == 1:
114
+ axis_mean = axis_data
115
+ axis_err_mean = np.array(axis_err, dtype=float) if axis_err is not None else None
116
+ else:
117
+ axis_mean = np.nanmean(axis_data, axis=-1)
118
+ if axis_err is not None:
119
+ axis_err = np.array(axis_err, dtype=float)
120
+ if axis_err.shape != axis_data.shape:
121
+ n_trials = axis_data.shape[-1]
122
+ axis_err_mean = axis_data.std(axis=-1, ddof=1) / np.sqrt(n_trials)
123
+ else:
124
+ n_trials = axis_data.shape[-1]
125
+ axis_err_mean = np.nanmean(axis_err, axis=-1) / np.sqrt(n_trials)
126
+ else:
127
+ n_trials = axis_data.shape[-1]
128
+ axis_err_mean = axis_data.std(axis=-1, ddof=1) / np.sqrt(n_trials)
129
+ return axis_mean, axis_err_mean
130
+
131
+ @staticmethod
132
+ def _normalize_p0(funcs, p0):
133
+ if p0 is None: return [None] * len(funcs)
134
+ if len(funcs) == 1: return [list(p0) if isinstance(p0, (list, tuple, np.ndarray)) else [p0]]
135
+ if all(isinstance(x, (list, tuple, np.ndarray)) for x in p0): return list(p0)
136
+ return [list(p0)] * len(funcs)
137
+
138
+ @staticmethod
139
+ def _normalize_list(item, n):
140
+ if item is None: return [None] * n
141
+ if isinstance(item, (str, tuple)): return [item] * n
142
+ return list(item)
143
+
144
+ @staticmethod
145
+ def _normalize_labels(labels, n):
146
+ if labels is None: return [f"Fit {i+1}" for i in range(n)]
147
+ if isinstance(labels, str): return [labels] * n
148
+ return list(labels)
149
+
150
+ @staticmethod
151
+ def _collapse_error(err_array: np.ndarray) -> np.ndarray:
152
+ err_array = np.array(err_array, dtype=float)
153
+
154
+ if err_array.ndim == 0:
155
+ return err_array
156
+ elif err_array.ndim == 1:
157
+ return err_array
158
+ elif err_array.shape[0] == 2 and err_array.ndim == 2:
159
+ return err_array
160
+ else:
161
+ collapsed = np.nanmean(err_array, axis=tuple(range(1, err_array.ndim)))
162
+ return collapsed
@@ -0,0 +1,72 @@
1
+ import numpy as np
2
+ from sympy import diff, lambdify, Symbol, Expr
3
+ from typing import Dict, Sequence, Union
4
+ from .error import Error
5
+
6
+ NumberArray = Union[float, int, np.ndarray, Sequence[float]]
7
+
8
+ class Propagation:
9
+ def __init__(self, error_objs: Dict[Symbol, Error] = None, axis=-1, manual_err_data = None, manual_err = None):
10
+ self._error_objs = error_objs
11
+ self.axis = axis
12
+ self.manual_err = manual_err
13
+ self.manual_err_data = manual_err_data
14
+
15
+ if error_objs is not None:
16
+ self._prepare_errors()
17
+
18
+
19
+ def _prepare_errors(self):
20
+ self.err_data = {}
21
+ self.std_err = {}
22
+ for symbol, err_obj in self._error_objs.items():
23
+ if not hasattr(err_obj, "err_data"):
24
+ raise TypeError(f"{symbol} is not an Error instance.")
25
+ if getattr(err_obj, "std_err", None) is None:
26
+ err_obj.std_err = err_obj.err(axis=self.axis)
27
+ data = np.array(err_obj.err_data, dtype=float)
28
+ stde = np.array(err_obj.std_err, dtype=float)
29
+
30
+ if stde.ndim < data.ndim:
31
+ stde = np.expand_dims(stde, axis=self.axis)
32
+
33
+ self.err_data[symbol] = data
34
+ self.std_err[symbol] = stde
35
+
36
+ def propagator(self, func: Expr, vars_in_func=None) -> np.ndarray:
37
+ if self.manual_err_data is not None and self.manual_err is not None:
38
+ if vars_in_func is None:
39
+ raise ValueError("Provide 'vars_in_func' when using manual error mode.")
40
+ sigma_sq_sum = 0
41
+
42
+ for v in vars_in_func:
43
+ if v not in self.manual_err_data or v not in self.manual_err:
44
+ raise ValueError(f"Missing data or error for variable {v}")
45
+
46
+ partial = diff(func, v)
47
+ arg_arrays = [np.array(self.manual_err_data[var], dtype=float) for var in vars_in_func]
48
+ f_partial = lambdify(vars_in_func, partial, modules=["numpy"])
49
+ partial_vals = f_partial(*arg_arrays)
50
+ sigma_sq_sum += (partial_vals * self.manual_err[v]) ** 2
51
+ if isinstance(sigma_sq_sum, (Expr, Symbol)):
52
+ sigma_sq_sum = float(sigma_sq_sum.evalf())
53
+
54
+ return np.sqrt(sigma_sq_sum)
55
+
56
+ if self._error_objs is None:
57
+ raise ValueError("No Error objects provided for automatic mode.")
58
+
59
+ vars_in_func = sorted(func.free_symbols, key=lambda s: s.name)
60
+ missing = [v for v in vars_in_func if v not in self._error_objs]
61
+ if missing:
62
+ raise ValueError(f"Missing Error objects for variables: {missing}")
63
+
64
+ sigma_sq_sum = 0
65
+ for v in vars_in_func:
66
+ partial = diff(func, v)
67
+ f_partial = lambdify(vars_in_func, partial, "numpy")
68
+ arg_arrays = [self.err_data[var] for var in vars_in_func]
69
+ partial_vals = f_partial(*arg_arrays)
70
+ sigma_sq_sum += (partial_vals * self.std_err[v]) ** 2
71
+
72
+ return np.sqrt(sigma_sq_sum)
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: physplot
3
+ Version: 0.1.0.post1
4
+ Summary: Plotting, curve fitting, and error propagation tools for physics labs
5
+ Author: Anant Mathur
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: numpy
8
+ Requires-Dist: matplotlib
9
+ Requires-Dist: scipy
10
+ Requires-Dist: sympy
11
+
12
+ # physplot
13
+
14
+ Plotting and error propagation tools for physics labs.
15
+
16
+ ## Installation
17
+ ```bash
18
+ pip install physplot
@@ -0,0 +1,8 @@
1
+ physplot/__init__.py,sha256=5qASELP-j0qDQqEHIJmKTJ9Y8AiFDyewX0tsWcYHyuw,137
2
+ physplot/error.py,sha256=kCzDL32bEp7FX2rZ6dClYkLDiTdyJuKmW0kWxidOt9M,1131
3
+ physplot/graph.py,sha256=Epk3EKnNqSjphyueqSplBAjHGz8sFzRsxPtK_GNYEGk,7196
4
+ physplot/propagation.py,sha256=E8gwYFIu0QFSW57tTPXxO_PIvsUgIjhjbKuxPmOg9TA,3057
5
+ physplot-0.1.0.post1.dist-info/METADATA,sha256=LTY49O462MQRQ4UtcEBR6YNnzl9hvhfhODguIM0Q9og,419
6
+ physplot-0.1.0.post1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ physplot-0.1.0.post1.dist-info/top_level.txt,sha256=VtH5cHiFQIpdvvLGRXob9SQMZsTj5tMnwpMmjrsKnhg,9
8
+ physplot-0.1.0.post1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ physplot