Skip to content

Commit d5a6c78

Browse files
authored
Merge pull request #10 from histogrammar/use-new-language-independent-spec
Use new language independent spec
2 parents 664eedd + d961ca6 commit d5a6c78

File tree

7 files changed

+154
-12
lines changed

7 files changed

+154
-12
lines changed

docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@
4747
# built documents.
4848
#
4949
# The short X.Y version.
50-
version = "0.9-prerelease"
50+
version = "1.0.0"
5151
# The full version, including alpha/beta/rc tags.
52-
release = "0.9-prerelease"
52+
release = "1.0.0"
5353

5454
# The language for content autogenerated by Sphinx. Refer to documentation
5555
# for a list of supported languages.

histogrammar/primitives/bag.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class Bag(Factory, Container):
2626
2727
A bag is the appropriate data type for scatter plots: a container that collects raw values, maintaining multiplicity but not order. (A "bag" is also known as a "multiset.") Conceptually, it is a mapping from distinct raw values to the number of observations: when two instances of the same raw value are observed, one key is stored and their weights add.
2828
29-
Although the user-defined function may return scalar numbers, fixed-dimension vectors of numbers, or categorical strings, it may not mix types. Different Bag primitives in an analysis tree may collect different types.
29+
Although the user-defined function may return scalar numbers, fixed-dimension vectors of numbers, or categorical strings, it may not mix range types. For the purposes of Label and Index (which can only collect aggregators of a single type), bags with different ranges are different types.
3030
"""
3131

3232
@staticmethod
@@ -318,6 +318,9 @@ def __eq__(self, other):
318318
if isinstance(v1i, numbers.Real) and isinstance(v2i, numbers.Real):
319319
if not numeq(v1i, v2i):
320320
return False
321+
elif isinstance(v1i, basestring) and isinstance(v2i, basestring):
322+
if v1i != v2i:
323+
return False
321324
else:
322325
return False
323326
else:

histogrammar/primitives/collection.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,10 @@ def __init__(self, **pairs):
256256
contentType = list(pairs.values())[0].name
257257
if any(x.name != contentType for x in pairs.values()):
258258
raise ContainerException("all Label values must have the same type")
259+
if contentType == "Bag":
260+
rangeType = list(pairs.values())[0].range
261+
if any(x.range != rangeType for x in pairs.values()):
262+
raise ContainerException("all Label values must have the same type")
259263

260264
self.entries = 0.0
261265
self.pairs = pairs
@@ -631,6 +635,10 @@ def __init__(self, *values):
631635
contentType = values[0].name
632636
if any(x.name != contentType for x in values):
633637
raise ValueError("all Index values must have the same type")
638+
if contentType == "Bag":
639+
rangeType = values[0].range
640+
if any(x.range != rangeType for x in values):
641+
raise ValueError("all Index values must have the same type")
634642

635643
self.entries = 0.0
636644
self.values = values

histogrammar/util.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,9 @@ def floatOrNan(x):
140140

141141
def floatToJson(x):
142142
"""Custom rule for converting non-finite numbers to JSON as quoted strings: ``"inf"``, ``"-inf"``, and ``"nan"``. This avoids Python's bad habit of putting literal ``Infinity``, ``-Infinity``, and ``NaN`` in the JSON (without quotes)."""
143-
if math.isnan(x):
143+
if x in ("nan", "inf", "-inf"):
144+
return x
145+
elif math.isnan(x):
144146
return "nan"
145147
elif math.isinf(x) and x > 0.0:
146148
return "inf"
@@ -222,9 +224,10 @@ def function(datum):
222224
# fill the namespace with math.* functions
223225
context.update(math.__dict__)
224226

225-
# if you have Numpy, override the namespace with numpy.* functions
227+
# if you have Numpy, include numpy.* functions
226228
if numpy is not None:
227-
context.update(numpy.__dict__)
229+
context["numpy"] = numpy
230+
context["np"] = numpy
228231

229232
# if the datum is a dict, override the namespace with its dict keys
230233
if isinstance(datum, dict): # if it's a dict
@@ -244,13 +247,18 @@ def function(datum):
244247
except AttributeError:
245248
v, = varname # otherwise, use the one and only variable
246249
if v is None: # as the object (only discover it once)
247-
try:
248-
v, = set(c.co_names) - set(context.keys())
249-
except ValueError:
250+
v = set(c.co_names) - set(context.keys())
251+
if len(v) > 1:
250252
raise NameError("more than one unrecognized variable names in single-argument function: {0}".format(set(c.co_names) - set(context.keys())))
253+
elif len(v) == 0:
254+
v = None
255+
else:
256+
v = list(v)[0]
257+
251258
varname[0] = v
252259

253-
context.update({v: datum})
260+
if v is not None:
261+
context.update({v: datum})
254262

255263
return eval(c, context)
256264

histogrammar/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import re
1818

19-
__version__ = "0.9-prerelease"
19+
__version__ = "1.0.0"
2020

2121
version = __version__
2222

test/testbasic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from histogrammar import *
2323

24-
class TestOriginal(unittest.TestCase):
24+
class TestBasic(unittest.TestCase):
2525
simple = [3.4, 2.2, -1.8, 0.0, 7.3, -4.7, 1.6, 0.0, -3.0, -1.7]
2626

2727
class Struct(object):

test/testspec.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright 2016 DIANA-HEP
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
import json
18+
import math
19+
import sys
20+
import unittest
21+
import urllib2
22+
23+
from histogrammar import *
24+
import histogrammar.version
25+
26+
tolerance = 1e-12
27+
util.relativeTolerance = tolerance
28+
util.absoluteTolerance = tolerance
29+
30+
class TestSpec(unittest.TestCase):
31+
def compare(self, x, y, name):
32+
if Factory.fromJson(x) != Factory.fromJson(y):
33+
sys.stderr.write(" FAILED " + name + "\n")
34+
sys.stderr.write(" PYTHON | SPECIFICATION\n")
35+
left = json.dumps(x, sort_keys=True, indent=2)
36+
right = json.dumps(y, sort_keys=True, indent=2)
37+
for leftline, rightline in zip(left.split("\n"), right.split("\n")):
38+
if leftline != rightline:
39+
sys.stderr.write("{0:50s} > {1}\n".format(leftline, rightline))
40+
else:
41+
sys.stderr.write("{0:50s} | {1}\n".format(leftline, rightline))
42+
self.assertEqual(Factory.fromJson(x), Factory.fromJson(y))
43+
44+
def runTest(self):
45+
sys.stdout.write("Downloading expected results, generated by specification {0}...\n".format(histogrammar.version.specification))
46+
try:
47+
testdata = json.load(urllib2.urlopen("http://histogrammar.org/test/{0}/test-data.json".format(histogrammar.version.specification)))
48+
except Exception as err:
49+
sys.stdout.write("could not download http://histogrammar.org/test/{0}/test-data.json\nbecause of {1}: {2}\n".format(histogrammar.version.specification, err.__class__.__name__, str(err)))
50+
return
51+
try:
52+
testresults = json.load(urllib2.urlopen("http://histogrammar.org/test/{0}/test-results.json".format(histogrammar.version.specification)))
53+
except Exception as err:
54+
sys.stdout.write("could not download http://histogrammar.org/test/{0}/test-results.jsonbecause of {1}: {2}\n\n".format(histogrammar.version.specification, err.__class__.__name__, str(err)))
55+
return
56+
57+
for x in testdata:
58+
for k, v in x.items():
59+
if k != "strings" and v in ("nan", "inf", "-inf"):
60+
x[k] = float(v)
61+
62+
def stripNames(x):
63+
if hasattr(x, "quantity"):
64+
x.quantity.name = None
65+
elif hasattr(x, "quantityName"):
66+
x.quantityName = None
67+
for xi in x.children:
68+
stripNames(xi)
69+
70+
for testresult in testresults:
71+
sys.stderr.write(testresult["expr"] + "\n")
72+
73+
zero = testresult["zero-named"]
74+
one = testresult["one-named"]
75+
two = testresult["two-named"]
76+
77+
h1 = eval(testresult["expr"])
78+
h2 = eval(testresult["expr"])
79+
80+
self.compare(h1.toJson(), zero, "NAMED ZERO")
81+
self.compare((h1 + h1).toJson(), zero, "NAMED ZERO + ZERO")
82+
self.compare(h1.zero().toJson(), zero, "NAMED ZERO.zero()")
83+
84+
for x in testdata:
85+
h1.fill(x)
86+
h2.fill(x)
87+
self.compare(h1.toJson(), one, "NAMED ONE")
88+
self.compare(h1.zero().toJson(), zero, "NAMED ONE.zero()")
89+
self.compare((h1 + h1.zero()).toJson(), one, "NAMED ONE + ZERO")
90+
self.compare((h1.zero() + h1).toJson(), one, "NAMED ZERO + ONE")
91+
92+
self.compare((h1 + h2).toJson(), two, "NAMED TWO VIA PLUS")
93+
94+
for x in testdata:
95+
h1.fill(x)
96+
self.compare(h1.toJson(), two, "NAMED TWO VIA FILL")
97+
98+
zero = testresult["zero-anonymous"]
99+
one = testresult["one-anonymous"]
100+
two = testresult["two-anonymous"]
101+
102+
h1 = eval(testresult["expr"])
103+
stripNames(h1)
104+
h2 = eval(testresult["expr"])
105+
stripNames(h2)
106+
107+
self.compare(h1.toJson(), zero, "ANONYMOUS ZERO")
108+
self.compare((h1 + h1).toJson(), zero, "ANONYMOUS ZERO + ZERO")
109+
self.compare(h1.zero().toJson(), zero, "ANONYMOUS ZERO.zero()")
110+
111+
for x in testdata:
112+
h1.fill(x)
113+
h2.fill(x)
114+
self.compare(h1.toJson(), one, "ANONYMOUS ONE")
115+
self.compare(h1.zero().toJson(), zero, "ANONYMOUS ONE.zero()")
116+
self.compare((h1 + h1.zero()).toJson(), one, "ANONYMOUS ONE + ZERO")
117+
self.compare((h1.zero() + h1).toJson(), one, "ANONYMOUS ZERO + ONE")
118+
119+
self.compare((h1 + h2).toJson(), two, "ANONYMOUS TWO VIA PLUS")
120+
121+
for x in testdata:
122+
h1.fill(x)
123+
self.compare(h1.toJson(), two, "ANONYMOUS TWO VIA FILL")

0 commit comments

Comments
 (0)