Skip to content

Commit 445e95d

Browse files
committed
add symlog axis for binning with tests
1 parent 3ff8a83 commit 445e95d

File tree

2 files changed

+432
-0
lines changed

2 files changed

+432
-0
lines changed
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
/* Copyright 2025-2025 Tapish Narwal
2+
*
3+
* This file is part of PIConGPU.
4+
*
5+
* PIConGPU is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* PIConGPU is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with PIConGPU.
17+
* If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
#pragma once
21+
22+
#include "picongpu/plugins/binning/UnitConversion.hpp"
23+
#include "picongpu/plugins/binning/axis/Axis.hpp"
24+
25+
#include <pmacc/memory/buffers/HostDeviceBuffer.hpp>
26+
27+
#include <array>
28+
#include <cmath>
29+
#include <cstdint>
30+
#include <stdexcept>
31+
#include <string>
32+
#include <type_traits>
33+
#include <vector>
34+
35+
namespace picongpu::plugins::binning::axis
36+
{
37+
/**
38+
* Symlog (symmetric log) axis with linear behavior near zero and logarithmic behavior for large values.
39+
* Similar to matplotlib's symlog scale.
40+
* The axis has a linear region within [-linearThreshold, linearThreshold] and logarithmic regions outside.
41+
* Bins are distributed to maintain smooth transitions at the threshold boundaries.
42+
* If overflow bins are enabled, allocates 2 extra bins for under and overflow.
43+
*/
44+
template<typename T_Attribute, typename T_AttrFunctor>
45+
class SymlogAxis
46+
{
47+
private:
48+
/**
49+
* Transform value to symlog space
50+
* For |val| <= threshold: returns val/threshold (normalized linear)
51+
* For |val| > threshold: returns sign(val) * (1 + log(|val|/threshold))
52+
*/
53+
static double symlogTransform(double val, double threshold)
54+
{
55+
if(std::abs(val) <= threshold)
56+
{
57+
return val / threshold;
58+
}
59+
else
60+
{
61+
return std::copysign(1.0 + std::log(std::abs(val) / threshold), val);
62+
}
63+
}
64+
65+
/**
66+
* Inverse transform from symlog space to real space
67+
*/
68+
static double symlogInverseTransform(double y, double threshold)
69+
{
70+
if(std::abs(y) <= 1.0)
71+
{
72+
return y * threshold;
73+
}
74+
else
75+
{
76+
return std::copysign(threshold * std::exp(std::abs(y) - 1.0), y);
77+
}
78+
}
79+
80+
void initBinEdges()
81+
{
82+
binEdges.reserve(axisSplit.nBins + 1);
83+
84+
// Transform min and max to symlog space
85+
double yMin = symlogTransform(axisSplit.m_range.min, linearThresholdSI);
86+
double yMax = symlogTransform(axisSplit.m_range.max, linearThresholdSI);
87+
double yRange = yMax - yMin;
88+
89+
// Create evenly spaced bins in transformed space
90+
for(size_t i = 0; i <= axisSplit.nBins; i++)
91+
{
92+
double y = yMin + (i * yRange) / axisSplit.nBins;
93+
double edge = symlogInverseTransform(y, linearThresholdSI);
94+
binEdges.emplace_back(edge);
95+
}
96+
}
97+
98+
public:
99+
using Type = T_Attribute;
100+
using ScalingType = std::conditional_t<
101+
std::is_integral_v<T_Attribute>,
102+
std::conditional_t<sizeof(T_Attribute) <= 4, float_X, double>,
103+
T_Attribute>;
104+
105+
AxisSplitting<T_Attribute> axisSplit;
106+
/** Axis name, written out to OpenPMD */
107+
std::string label;
108+
/** Units(Dimensionality) of the axis */
109+
std::array<double, numUnits> units;
110+
/** Linear threshold in SI units */
111+
double linearThresholdSI;
112+
/** axisSplit.nBins + 1 bin edges in SI units */
113+
std::vector<double> binEdges;
114+
115+
struct SymlogAxisKernel
116+
{
117+
/** Function to place particle on axis */
118+
T_AttrFunctor getAttributeValue;
119+
/** Linear threshold in PIC units */
120+
ScalingType linearThresholdPIC;
121+
/** 1/linearThresholdPIC */
122+
ScalingType invLinearThreshold;
123+
/** Range in PIC units in symlog space */
124+
Range<ScalingType> symlogPicRange;
125+
// 1/yRange
126+
ScalingType invYRange;
127+
/** Enable or disable allocation of extra bins for out of range particles */
128+
bool overflowEnabled;
129+
/** Number of bins excluding overflow */
130+
uint32_t nBins;
131+
132+
constexpr SymlogAxisKernel(
133+
T_AttrFunctor const& attrFunc,
134+
AxisSplitting<T_Attribute> const& axisSplit,
135+
double linearThreshold,
136+
std::array<double, numUnits> const& unitsArr)
137+
: getAttributeValue{attrFunc}
138+
, linearThresholdPIC{static_cast<ScalingType>(toPICUnits(linearThreshold, unitsArr))}
139+
, invLinearThreshold{ScalingType(1) / linearThresholdPIC}
140+
, symlogPicRange{symlogTransformKernel(toPICUnits(axisSplit.m_range.min, unitsArr)), symlogTransformKernel(toPICUnits(axisSplit.m_range.max, unitsArr))}
141+
, invYRange{ ScalingType(1) / (symlogPicRange.max - symlogPicRange.min)}
142+
, overflowEnabled{axisSplit.enableOverflowBins}
143+
, nBins{axisSplit.nBins}
144+
{
145+
}
146+
147+
/**
148+
* Kernel-side symlog transform
149+
*/
150+
ALPAKA_FN_HOST_ACC ScalingType symlogTransformKernel(ScalingType val) const
151+
{
152+
ScalingType absVal = math::abs(val);
153+
154+
// Compute both branches
155+
ScalingType linear = val * invLinearThreshold;
156+
ScalingType logarithmic = math::copysign(ScalingType(1) + math::log(absVal * invLinearThreshold), val);
157+
158+
// Check if in linear region
159+
ScalingType isLinear = ScalingType(absVal <= linearThresholdPIC);
160+
161+
return isLinear ? linear : logarithmic;
162+
}
163+
164+
template<typename... Args>
165+
ALPAKA_FN_HOST_ACC std::pair<bool, uint32_t> getBinIdx(Args const&... args) const
166+
{
167+
auto val = getAttributeValue(args...);
168+
auto sVal = static_cast<ScalingType>(val);
169+
ScalingType y = symlogTransformKernel(sVal);
170+
171+
uint32_t inRange = (y >= symlogPicRange.min) & (y < symlogPicRange.max);
172+
// uint32_t belowMin = (y < symlogPicRange.min);
173+
uint32_t aboveMax = (y >= symlogPicRange.max);
174+
175+
ScalingType normalized = (y - symlogPicRange.min) * invYRange;
176+
177+
uint32_t rangeIdx = math::min(static_cast<uint32_t>(normalized * nBins), nBins - 1);
178+
179+
if(overflowEnabled)
180+
{
181+
// underflow*0 + inRange*(rangeIdx+1) + aboveMax*(nBins-1)
182+
// I dont compute underflow/belowMin since it is always zero
183+
uint32_t binIdx = inRange * (rangeIdx + 1) + aboveMax * (nBins + 1);
184+
return {true, binIdx};
185+
}
186+
else
187+
{
188+
return {inRange, rangeIdx};
189+
}
190+
}
191+
};
192+
193+
SymlogAxisKernel sAK;
194+
195+
SymlogAxis(
196+
AxisSplitting<T_Attribute> const& axSplit,
197+
T_AttrFunctor const& attrFunctor,
198+
double linearThreshold,
199+
std::string const& label,
200+
std::array<double, numUnits> const& units)
201+
: axisSplit{axSplit}
202+
, label{label}
203+
, units{units}
204+
, linearThresholdSI{linearThreshold}
205+
, sAK{attrFunctor, axisSplit, linearThreshold, units}
206+
{
207+
if(linearThreshold <= 0)
208+
{
209+
throw std::domain_error("Linear threshold must be positive");
210+
}
211+
initBinEdges();
212+
}
213+
214+
constexpr uint32_t getNBins() const
215+
{
216+
return sAK.nBins;
217+
}
218+
219+
double getUnitConversion() const
220+
{
221+
return getConversionFactor(units);
222+
}
223+
224+
SymlogAxisKernel getAxisKernel() const
225+
{
226+
return sAK;
227+
}
228+
229+
/**
230+
* @return bin edges in SI units
231+
*/
232+
std::vector<double> getBinEdgesSI() const
233+
{
234+
return binEdges;
235+
}
236+
};
237+
238+
/**
239+
* @details Creates a symlog (symmetric log) axis
240+
* @tparam T_Attribute Type of the attribute
241+
* @param axisSplitting Range and number of bins
242+
* @param linearThreshold Threshold for linear region (in SI units)
243+
* @param functorDescription Functor and metadata
244+
*/
245+
template<typename T_Attribute, typename T_FunctorDescription>
246+
HINLINE auto createSymlog(
247+
AxisSplitting<T_Attribute> const& axSplit,
248+
double linearThreshold,
249+
T_FunctorDescription const& functorDesc)
250+
{
251+
static_assert(
252+
std::is_same_v<typename T_FunctorDescription::QuantityType, T_Attribute>,
253+
"Access functor return type and range type should be the same");
254+
255+
return SymlogAxis<T_Attribute, typename T_FunctorDescription::FunctorType>(
256+
axSplit,
257+
functorDesc.functor,
258+
linearThreshold,
259+
functorDesc.name,
260+
functorDesc.units);
261+
}
262+
263+
} // namespace picongpu::plugins::binning::axis

0 commit comments

Comments
 (0)