Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#### User changes

* Histogram and Axis classes now follow PEP 8 naming scheme (`histogram`->`Histogram`, `regular`->`Regular`, etc.) [#192][]
* You can now view a histogram with accumulators, with property access such as `h.view().value` [#194][]
* Added axes transforms [#192][]
* You can now sum over a range with endpoints [#185][]
* You can now access the functional regular axis directly, `regular_sqrt` becomes `regular.sqrt`, etc.. [#183][]
Expand All @@ -12,6 +13,10 @@
* Signatures are much nicer in Python 3 [#188][]


#### Bug fixes:
* Unlimited and AtomicInt storages now allow single item access [#194][]
* `.view()` now no longer makes a copy [#194][]

#### Developer changes

* The hist/axis classes are now pure Python, with a C++ object inside [#183][]
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ Python bindings for [Boost::Histogram][] ([source][Boost::Histogram source]), a
> Join the [discussion on gitter][gitter-link] or [open an issue](https://github.com/scikit-hep/boost-histogram/issues)!
>
> #### Known issues (develop):
> * Non-simple storages do not support `.view()` or the buffer interface; you can access and set one element at a time.
> * Setting with an array is not yet supported (`h[...] = np.array(...)`).


Expand Down
9 changes: 4 additions & 5 deletions boost_histogram/_internal/hist.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from .. import _core
from .axis import _to_axis, Axis
from .view import _to_view
from .axistuple import AxesTuple
from .sig_tools import inject_signature
from .storage import Double
Expand Down Expand Up @@ -135,9 +136,7 @@ def __repr__(self):
return self.__class__.__name__ + repr(self._hist)[9:]

def __array__(self):
return np.asarray(self._hist)
# TODO: .view does not seem to return an editable view
# so we have to use the buffer interface here
return self.view()

def __add__(self, other):
return self.__class__(self._hist + other._hist)
Expand Down Expand Up @@ -296,7 +295,7 @@ def view(self, flow=False):
"""
Return a view into the data, optionally with overflow turned on.
"""
return self._hist.view(flow)
return _to_view(self._hist.view(flow))

def reset(self):
"""
Expand Down Expand Up @@ -406,5 +405,5 @@ def __getitem__(self, index):
)

def __setitem__(self, index, value):
indexes = _compute_commonindex(self._hist, index, expand=True)
indexes = _compute_commonindex(self._hist, index, expand=False)
self._hist._at_set(value, *indexes)
120 changes: 120 additions & 0 deletions boost_histogram/_internal/view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from __future__ import absolute_import, division, print_function

from ..accumulators import Mean, WeightedMean, WeightedSum

import numpy as np


class View(np.ndarray):
__slots__ = ()

def __getitem__(self, ind):
sliced = super(View, self).__getitem__(ind)

# If the shape is empty, return the parent type
if not sliced.shape:
return self._PARENT._make(*sliced)
# If the dtype has changed, return a normal array (no longer a record)
elif sliced.dtype != self.dtype:
return np.asarray(sliced)
# Otherwise, no change, return the same View type
else:
return sliced

def __repr__(self):
# Numpy starts the ndarray class name with "array", so we replace it
# with our class name
return (
"{self.__class__.__name__}(\n ".format(self=self)
+ repr(self.view(np.ndarray))[6:]
)

def __str__(self):
fields = ", ".join(self._FIELDS)
return "{self.__class__.__name__}: ({fields})\n{arr}".format(
self=self, fields=fields, arr=self.view(np.ndarray)
)


def fields(*names):
"""
This decorator adds the name to the _FIELDS
class property (for printing in reprs), and
adds a property that looks like this:

@property
def name(self):
return self["name"]
@name.setter
def name(self, value):
self["name"] = value
"""

def injector(cls):
if hasattr(cls, "_FIELDS"):
raise RuntimeError(
"{0} already has had a fields decorator applied".format(
self.__class__.__name__
)
)
fields = []
for name in names:
fields.append(name)

def fget(self):
return self[name]

def fset(self, value):
self[name] = value

setattr(cls, name, property(fget, fset))
cls._FIELDS = tuple(fields)
return cls

return injector


@fields("value", "variance")
class WeightedSumView(View):
__slots__ = ()
_PARENT = WeightedSum


@fields(
"sum_of_weights",
"sum_of_weights_squared",
"value",
"sum_of_weighted_deltas_squared",
)
class WeightedMeanView(View):
__slots__ = ()
_PARENT = WeightedMean

@property
def variance(self):
return self["sum_of_weighted_deltas_squared"] / (
self["sum_of_weights"]
- self["sum_of_weights_squared"] / self["sum_of_weights"]
)


@fields("count", "value", "sum_of_deltas_squared")
class MeanView(View):
__slots__ = ()
_PARENT = Mean

# Variance is a computation
@property
def variance(self):
return self["sum_of_deltas_squared"] / (self["count"] - 1)


def _to_view(item, value=False):
for cls in View.__subclasses__():
if cls._FIELDS == item.dtype.names:
ret = item.view(cls)
if value and ret.shape:
return ret.value
else:
return ret
return item
138 changes: 138 additions & 0 deletions include/boost/histogram/python/accumulators/mean.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Copyright 2015-2019 Hans Dembinski and Henry Schreiner
//
// Distributed under the Boost Software License, version 1.0.
// (See accompanying file LICENSE_1_0.txt
// or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// Based on boost/histogram/accumulators/mean.hpp
// Changes:
// * Internal values are public for access from Python
// * A special constructor added for construction from Python

#pragma once

#include <boost/assert.hpp>
#include <boost/core/nvp.hpp>
#include <boost/histogram/fwd.hpp> // for mean<>
#include <boost/throw_exception.hpp>
#include <stdexcept>
#include <type_traits>

namespace boost {
namespace histogram {
namespace python {

/** Calculates mean and variance of sample.
Uses Welfords's incremental algorithm to improve the numerical
stability of mean and variance computation.
*/
template <class RealType>
struct mean {
public:
mean() = default;
mean(const RealType &n, const RealType &mean, const RealType &variance) noexcept
: sum_(n)
, mean_(mean)
, sum_of_deltas_squared_(variance * (n - 1)) {}

mean(const RealType &sum,
const RealType &mean,
const RealType &sum_of_deltas_squared,
bool /* Tag to trigger python internal constructor */)
: sum_(sum)
, mean_(mean)
, sum_of_deltas_squared_(sum_of_deltas_squared) {}

void operator()(const RealType &x) noexcept {
sum_ += static_cast<RealType>(1);
const auto delta = x - mean_;
mean_ += delta / sum_;
sum_of_deltas_squared_ += delta * (x - mean_);
}

void operator()(const weight_type<RealType> &w, const RealType &x) noexcept {
sum_ += w.value;
const auto delta = x - mean_;
mean_ += w.value * delta / sum_;
sum_of_deltas_squared_ += w.value * delta * (x - mean_);
}

template <class T>
mean &operator+=(const mean<T> &rhs) noexcept {
if(sum_ != 0 || rhs.sum_ != 0) {
const auto tmp = mean_ * sum_ + static_cast<RealType>(rhs.mean_ * rhs.sum_);
sum_ += rhs.sum_;
mean_ = tmp / sum_;
}
sum_of_deltas_squared_ += static_cast<RealType>(rhs.sum_of_deltas_squared_);
return *this;
}

mean &operator*=(const RealType &s) noexcept {
mean_ *= s;
sum_of_deltas_squared_ *= s * s;
return *this;
}

template <class T>
bool operator==(const mean<T> &rhs) const noexcept {
return sum_ == rhs.sum_ && mean_ == rhs.mean_
&& sum_of_deltas_squared_ == rhs.sum_of_deltas_squared_;
}

template <class T>
bool operator!=(const mean<T> &rhs) const noexcept {
return !operator==(rhs);
}

const RealType &count() const noexcept { return sum_; }
const RealType &value() const noexcept { return mean_; }
RealType variance() const noexcept { return sum_of_deltas_squared_ / (sum_ - 1); }

template <class Archive>
void serialize(Archive &ar, unsigned version) {
if(version == 0) {
// read only
std::size_t sum;
ar &make_nvp("sum", sum);
sum_ = static_cast<RealType>(sum);
} else {
ar &make_nvp("sum", sum_);
}
ar &make_nvp("mean", mean_);
ar &make_nvp("sum_of_deltas_squared", sum_of_deltas_squared_);
}

RealType sum_ = 0, mean_ = 0, sum_of_deltas_squared_ = 0;
};

} // namespace python
} // namespace histogram
} // namespace boost

#ifndef BOOST_HISTOGRAM_DOXYGEN_INVOKED

namespace boost {
namespace serialization {

template <class T>
struct version;

// version 1 for boost::histogram::accumulators::mean<RealType>
template <class RealType>
struct version<histogram::python::mean<RealType>> : std::integral_constant<int, 1> {};

} // namespace serialization
} // namespace boost

namespace std {
template <class T, class U>
/// Specialization for boost::histogram::accumulators::mean.
struct common_type<boost::histogram::accumulators::mean<T>,
boost::histogram::accumulators::mean<U>> {
using type = boost::histogram::accumulators::mean<common_type_t<T, U>>;
};
} // namespace std

#endif
Loading