Skip to content

Commit 90039ea

Browse files
Pepesobjpivarskipre-commit-ci[bot]
authored
feat: implement TGraph-writing (#1256)
* feat: implement TGraph-writing * style: pre-commit fixes * feat: implemented as_TGraph funciton * to_TGraph() checkpoint * to_TGraph() checkpoint * to_TGraph() checkpoint * Functionality implemented, basic tests done * changed to_TGraph function placement * By default line of the graph won't touch the edge of the chart * Removed my script from tracking * Handle 0 length arrays * Test saving TGraph to .root file and test reading with ROOT * style: pre-commit fixes * fix: test failing because of imports and added license Co-authored-by: Jim Pivarski <[email protected]> * change: using pytest.approx() to compare floats Co-authored-by: Jim Pivarski <[email protected]> * change: remove unused epsilon value Co-authored-by: Jim Pivarski <[email protected]> * style: pre-commit fixes * Update tests/test_1128_TGraph_writing.py Co-authored-by: Jim Pivarski <[email protected]> * Change: changed error message Co-authored-by: Jim Pivarski <[email protected]> * Change: change error message Co-authored-by: Jim Pivarski <[email protected]> * Change: change error message Co-authored-by: Jim Pivarski <[email protected]> * Change: change error message Co-authored-by: Jim Pivarski <[email protected]> * change: using np.min/np.max instead of builtin min/max * style: pre-commit fixes * change: check if minY, maxY is instance of numbers.Real * style: pre-commit fixes * change: changed function name from to_TGraph() to as_TGraph (better reflects what it does: interpret as what? -> TGraph) * Change: improve error messages * Fixed error messages * edit error messages for consistency and clarity * style: pre-commit fixes --------- Co-authored-by: Jim Pivarski <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jim Pivarski <[email protected]>
1 parent caa021c commit 90039ea

File tree

4 files changed

+386
-1
lines changed

4 files changed

+386
-1
lines changed

src/uproot/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@
125125
from uproot.writing import to_writable
126126
from uproot.writing import dask_write
127127

128+
from uproot.writing.interpret import as_TGraph
129+
128130
import uproot.models.TObject
129131
import uproot.models.TString
130132
import uproot.models.TArray

src/uproot/models/TGraph.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,18 @@ def _serialize(self, out, header, name, tobject_flags):
336336
where = len(out)
337337
for x in self._bases:
338338
x._serialize(out, True, name, tobject_flags)
339-
raise NotImplementedError("FIXME")
339+
out.extend(
340+
[
341+
struct.pack(">i", self._members["fNpoints"]),
342+
b"\x01",
343+
self._members["fX"].astype(">f8").tobytes(),
344+
b"\x01",
345+
self._members["fY"].astype(">f8").tobytes(),
346+
b"@\x00\x00\x1f\xff\xff\xff\xffTList\x00@\x00\x00\x11\x00\x05\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
347+
struct.pack(">d", self._members["fMinimum"]),
348+
struct.pack(">d", self._members["fMaximum"]),
349+
]
350+
)
340351
if header:
341352
num_bytes = sum(len(x) for x in out[where:])
342353
version = 4

src/uproot/writing/interpret.py

+312
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
from __future__ import annotations
2+
3+
import numbers
4+
5+
import numpy as np
6+
7+
import uproot
8+
9+
10+
def _as_TGraph(
11+
x,
12+
y,
13+
x_errors=None,
14+
y_errors=None,
15+
x_errors_low=None,
16+
x_errors_high=None,
17+
y_errors_low=None,
18+
y_errors_high=None,
19+
title="",
20+
xAxisLabel="",
21+
yAxisLabel="",
22+
minY=None,
23+
maxY=None,
24+
lineColor: int = 602,
25+
lineStyle: int = 1,
26+
lineWidth: int = 1,
27+
fillColor: int = 0,
28+
fillStyle: int = 1001,
29+
markerColor: int = 1,
30+
markerStyle: int = 1,
31+
markerSize: float = 1.0,
32+
):
33+
"""
34+
Args:
35+
x (1D numpy.ndarray): x values of TGraph (length of x and y has to be the same).
36+
y (1D numpy.ndarray): y values of TGraph (length of x and y has to be the same).
37+
x_errors(None or 1D numpy.ndarray): Symethrical values of errors for corresponding x value (length of x_errors has to be the same as x and y)
38+
y_errors(None or 1D numpy.ndarray): Symethrical values of errors for corresponding y value (length of y_errors has to be the same as x and y)
39+
x_errors_low(None or 1D numpy.ndarray): Asymmetrical lower values of errors for corresponding x value (length of x_errors_low has to be the same as x and y)
40+
x_errors_high(None or 1D numpy.ndarray): Asymmetrical upper values of errors for corresponding x value (length of x_errors_high has to be the same as x and y)
41+
y_errors_low(None or 1D numpy.ndarray): Asymmetrical lower values of errors for corresponding y value (length of y_errors_low has to be the same as x and y)
42+
y_errors_high(None or 1D numpy.ndarray): Asymmetrical upper values of errors for corresponding y value (length of y_errors_high has to be the same as x and y)
43+
title (str): Title of the histogram.
44+
xAxisLabel (str): Label of the X axis.
45+
yAxisLabel (str): Label of the Y axis.
46+
minY (None or float): Minimum value on the Y axis to be shown, if set to None then minY=min(y)
47+
maxY (None or float): Maximum value on the Y axis to be shown, if set to None then maxY=max(y)
48+
lineColor (int): Line color. (https://root.cern.ch/doc/master/classTAttLine.html)
49+
lineStyle (int): Line style.
50+
lineWidth (int): Line width.
51+
fillColor (int): Fill area color. (https://root.cern.ch/doc/master/classTAttFill.html)
52+
fillStyle (int): Fill area style.
53+
markerColor (int): Marker color. (https://root.cern.ch/doc/master/classTAttMarker.html)
54+
markerStyle (int): Marker style.
55+
markerSize (float): Marker size.
56+
57+
WARNING! This function only works for TGraph, because serialization of TGraphErrors and TGraphAsymmErrors is not implemented yet.
58+
59+
Function that converts arguments into TGraph, TGraphErrors or TGraphAsymmErros based on the given arguments.
60+
When all errors are unspecified, detected object is TGraph.
61+
When x_errors, y_errors are specified, detected object is TGraphErrors.
62+
When x_errors_low, x_errors_high, y_errors_low, y_errors_high are specified, detected object is TGraphAsymmErrors.
63+
Note that both x_errors, y_errors need to be specified or set to None.
64+
The same rule applies to x_errors_low, x_errors_high, y_errors_low, y_errors_high.
65+
Also can't specify x_errors, y_errors and x_errors_low, x_errors_high, y_errors_low, y_errors_high at the same time.
66+
All rules are designed to remove any ambiguity.
67+
"""
68+
69+
sym_errors = [x_errors, y_errors]
70+
sym_errors_bool = [err is not None for err in sym_errors]
71+
72+
asym_errors = [x_errors_low, x_errors_high, y_errors_low, y_errors_high]
73+
asym_errors_bool = [err is not None for err in asym_errors]
74+
75+
tgraph_type = "TGraph"
76+
77+
# Detecting which type of TGraph to chose
78+
if any(sym_errors_bool):
79+
if not all(sym_errors_bool):
80+
raise ValueError("uproot.as_TGraph requires both x_errors and y_errors")
81+
if any(asym_errors_bool):
82+
raise ValueError(
83+
"uproot.as_TGraph can accept symmetrical errors OR asymmetrical errors, but not both"
84+
)
85+
tgraph_type = "TGraphErrors"
86+
87+
elif any(asym_errors_bool):
88+
if not all(asym_errors_bool):
89+
raise ValueError(
90+
"uproot.as_TGraph requires all of the following: x_errors_low, x_errors_high, y_errors_low, y_errors_high"
91+
)
92+
if any(sym_errors_bool):
93+
raise ValueError(
94+
"uproot.as_TGraph can accept symmetrical errors OR asymmetrical errors, but not both"
95+
)
96+
tgraph_type = "TGraphAsymmErrors"
97+
98+
tobject = uproot.models.TObject.Model_TObject.empty()
99+
100+
tnamed = uproot.models.TNamed.Model_TNamed.empty()
101+
tnamed._deeply_writable = True
102+
tnamed._bases.append(tobject)
103+
tnamed._members["fName"] = (
104+
"" # Temporary name, will be overwritten by the writing process because Uproot's write syntax is ``file[name] = histogram``
105+
)
106+
# Constraint so user won't break TGraph naming
107+
if ";" in title or ";" in xAxisLabel or ";" in yAxisLabel:
108+
raise ValueError("title and xAxisLabel and yAxisLabel can't contain ';'!")
109+
fTitle = f"{title};{xAxisLabel};{yAxisLabel}"
110+
tnamed._members["fTitle"] = fTitle
111+
112+
# setting line styling
113+
tattline = uproot.models.TAtt.Model_TAttLine_v2.empty()
114+
tattline._deeply_writable = True
115+
tattline._members["fLineColor"] = lineColor
116+
tattline._members["fLineStyle"] = lineStyle
117+
tattline._members["fLineWidth"] = lineWidth
118+
119+
# setting filling styling, does not do anything to TGraph
120+
tattfill = uproot.models.TAtt.Model_TAttFill_v2.empty()
121+
tattfill._deeply_writable = True
122+
tattfill._members["fFillColor"] = fillColor
123+
tattfill._members["fFillStyle"] = fillStyle
124+
125+
# setting marker styling, those are points on graph
126+
tattmarker = uproot.models.TAtt.Model_TAttMarker_v2.empty()
127+
tattmarker._deeply_writable = True
128+
tattmarker._members["fMarkerColor"] = markerColor
129+
tattmarker._members["fMarkerStyle"] = markerStyle
130+
tattmarker._members["fMarkerSize"] = markerSize
131+
132+
if len(x) != len(y):
133+
raise ValueError("Arrays x and y must have the same length!")
134+
if len(x) == 0:
135+
raise ValueError("uproot.as_TGraph x and y arrays can't be empty")
136+
if len(x.shape) != 1:
137+
raise ValueError(f"x has to be 1D, but is {len(x.shape)}D!")
138+
if len(y.shape) != 1:
139+
raise ValueError(f"y has to be 1D, but is {len(y.shape)}D!")
140+
141+
if minY is None:
142+
new_minY = np.min(x)
143+
elif not isinstance(minY, numbers.Real):
144+
raise ValueError(
145+
f"uproot.as_TGraph minY has to be None or a number, not {type(minY)}"
146+
)
147+
else:
148+
new_minY = minY
149+
150+
if maxY is None:
151+
new_maxY = np.max(x)
152+
elif not isinstance(maxY, numbers.Real):
153+
raise ValueError(
154+
f"uproot.as_TGraph minY has to be None or a number, not {type(maxY)}"
155+
)
156+
else:
157+
new_maxY = maxY
158+
159+
tGraph = uproot.models.TGraph.Model_TGraph_v4.empty()
160+
161+
tGraph._bases.append(tnamed)
162+
tGraph._bases.append(tattline)
163+
tGraph._bases.append(tattfill)
164+
tGraph._bases.append(tattmarker)
165+
166+
tGraph._members["fNpoints"] = len(x)
167+
tGraph._members["fX"] = x
168+
tGraph._members["fY"] = y
169+
tGraph._members["fMinimum"] = (
170+
minY if minY is not None else new_minY - 0.1 * (new_maxY - new_minY)
171+
) # by default graph line wont touch the edge of the chart
172+
tGraph._members["fMaximum"] = (
173+
maxY if maxY is not None else new_maxY + 0.1 * (new_maxY - new_minY)
174+
) # by default graph line wont touch the edge of the chart
175+
176+
returned_TGraph = tGraph
177+
178+
if tgraph_type == "TGraphErrors":
179+
if not (len(x_errors) == len(y_errors) == len(x)):
180+
raise ValueError(
181+
"Length of all error arrays has to be the same as length of arrays X and Y"
182+
)
183+
tGraphErrors = uproot.models.TGraph.Model_TGraphErrors_v3.empty()
184+
tGraphErrors._bases.append(tGraph)
185+
tGraphErrors._members["fEX"] = x_errors
186+
tGraphErrors._members["fEY"] = y_errors
187+
188+
returned_TGraph = tGraphErrors
189+
elif tgraph_type == "TGraphAsymmErrors":
190+
if not (
191+
len(x_errors_low)
192+
== len(x_errors_high)
193+
== len(y_errors_low)
194+
== len(y_errors_high)
195+
== len(x)
196+
):
197+
raise ValueError(
198+
"Length of errors all error arrays has to be the same as length of arrays X and Y"
199+
)
200+
tGraphAsymmErrors = uproot.models.TGraph.Model_TGraphAsymmErrors_v3.empty()
201+
tGraphAsymmErrors._bases.append(tGraph)
202+
tGraphAsymmErrors._members["fEXlow"] = x_errors_low
203+
tGraphAsymmErrors._members["fEXhigh"] = x_errors_high
204+
tGraphAsymmErrors._members["fEYlow"] = y_errors_low
205+
tGraphAsymmErrors._members["fEYhigh"] = y_errors_high
206+
207+
returned_TGraph = tGraphAsymmErrors
208+
209+
return returned_TGraph
210+
211+
212+
def as_TGraph(
213+
df,
214+
title="",
215+
xAxisLabel="",
216+
yAxisLabel="",
217+
minY=None,
218+
maxY=None,
219+
lineColor: int = 602,
220+
lineStyle: int = 1,
221+
lineWidth: int = 1,
222+
markerColor: int = 1,
223+
markerStyle: int = 1,
224+
markerSize: float = 1.0,
225+
):
226+
"""
227+
Args:
228+
df (DataFrame or and dict like object): DataFrame object with column names as follows:
229+
x (float): x values of TGraph.
230+
y (float): y values of TGraph.
231+
x_errors (float or left unspecified): Symethrical error values for corresponding x value
232+
y_errors (float or left unspecified): Symethrical error values for corresponding y value
233+
x_errors_low (float or left unspecified): Asymmetrical lower error values for corresponding x value
234+
x_errors_high (float or left unspecified): Asymmetrical upper error values for corresponding x value
235+
y_errors_low (float or left unspecified): Asymmetrical lower error values for corresponding y value
236+
y_errors_high (float or left unspecified): Asymmetrical upper error values for corresponding y value
237+
(other column names will be ignored!)
238+
title (str): Title of the histogram.
239+
xAxisLabel (str): Label of the X axis.
240+
yAxisLabel (str): Label of the Y axis.
241+
minY (None or float): Minimum value on the Y axis to be shown, if set to None then minY=min(y)
242+
maxY (None or float): Maximum value on the Y axis to be shown, if set to None then maxY=max(y)
243+
lineColor (int): Line color. (https://root.cern.ch/doc/master/classTAttLine.html)
244+
lineStyle (int): Line style.
245+
lineWidth (int): Line width.
246+
markerColor (int): Marker color. (https://root.cern.ch/doc/master/classTAttMarker.html)
247+
markerStyle (int): Marker style.
248+
markerSize (float): Marker size.
249+
250+
WARNING! This function only works for TGraph, because serialization of TGraphErrors and TGraphAsymmErrors is not implemented yet.
251+
252+
Function that converts DataFrame into TGraph, TGraphErrors or TGraphAsymmErros based on the specified DataFrame columns.
253+
When all error columns are unspecified, detected object is TGraph.
254+
When x_errors, y_errors are specified, detected object is TGraphErrors.
255+
When x_errors_low, x_errors_high, y_errors_low, y_errors_high are specified, detected object is TGraphAsymmErrors.
256+
Note that both {x_errors, x_errors} need to be specified or set to None.
257+
The same rule applies {to x_errors_low, x_errors_high, x_errors_low, x_errors_high}.
258+
Also can't specify {x_errors, y_errors} and {x_errors_low, x_errors_high, y_errors_low, y_errors_high} at the same time.
259+
"""
260+
261+
x = np.array(df["x"]) if df.get("x", None) is not None else None
262+
y = np.array(df["y"]) if df.get("y", None) is not None else None
263+
x_errors = (
264+
np.array(df["x_errors"]) if df.get("x_errors", None) is not None else None
265+
)
266+
y_errors = (
267+
np.array(df["y_errors"]) if df.get("y_errors", None) is not None else None
268+
)
269+
x_errors_low = (
270+
np.array(df["x_errors_low"])
271+
if df.get("x_errors_low", None) is not None
272+
else None
273+
)
274+
x_errors_high = (
275+
np.array(df["x_errors_high"])
276+
if df.get("x_errors_high", None) is not None
277+
else None
278+
)
279+
y_errors_low = (
280+
np.array(df["y_errors_low"])
281+
if df.get("y_errors_low", None) is not None
282+
else None
283+
)
284+
y_errors_high = (
285+
np.array(df["y_errors_high"])
286+
if df.get("y_errors_high", None) is not None
287+
else None
288+
)
289+
290+
return _as_TGraph(
291+
x,
292+
y,
293+
x_errors,
294+
y_errors,
295+
x_errors_low,
296+
x_errors_high,
297+
y_errors_low,
298+
y_errors_high,
299+
title,
300+
xAxisLabel,
301+
yAxisLabel,
302+
minY,
303+
maxY,
304+
lineColor,
305+
lineStyle,
306+
lineWidth,
307+
0,
308+
1001,
309+
markerColor,
310+
markerStyle,
311+
markerSize,
312+
)

0 commit comments

Comments
 (0)