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 +5 -0
- physplot/error.py +31 -0
- physplot/graph.py +162 -0
- physplot/propagation.py +72 -0
- physplot-0.1.0.post1.dist-info/METADATA +18 -0
- physplot-0.1.0.post1.dist-info/RECORD +8 -0
- physplot-0.1.0.post1.dist-info/WHEEL +5 -0
- physplot-0.1.0.post1.dist-info/top_level.txt +1 -0
physplot/__init__.py
ADDED
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
|
physplot/propagation.py
ADDED
|
@@ -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 @@
|
|
|
1
|
+
physplot
|