Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 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
12 changes: 12 additions & 0 deletions pyphare/pyphare/core/gridlayout.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
"Pyz": "primal",
"Pzz": "primal",
"tags": "dual",
"value": "primal",
"x": "primal",
"y": "primal",
"z": "primal",
},
"y": {
"Bx": "dual",
Expand Down Expand Up @@ -79,6 +83,10 @@
"Pyz": "primal",
"Pzz": "primal",
"tags": "dual",
"value": "primal",
"x": "primal",
"y": "primal",
"z": "primal",
},
"z": {
"Bx": "dual",
Expand Down Expand Up @@ -114,6 +122,10 @@
"Pyz": "primal",
"Pzz": "primal",
"tags": "dual",
"value": "primal",
"x": "primal",
"y": "primal",
"z": "primal",
},
}
yee_centering_lower = {
Expand Down
5 changes: 3 additions & 2 deletions pyphare/pyphare/core/operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ def _compute_dot_product(patch_datas, **kwargs):


def _compute_sqrt(patch_datas, **kwargs):
ref_name = next(iter(patch_datas.keys()))
# ref_name = next(iter(patch_datas.keys())) TODO this is always "value"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there something to do here ?


dset = np.sqrt(patch_datas["value"][:])

return (
{"name": "value", "data": dset, "centering": patch_datas[ref_name].centerings},
{"name": "value", "data": dset, "centering": patch_datas["value"].centerings},
)


Expand Down Expand Up @@ -139,3 +139,4 @@ def grad(hier, **kwargs):
h = compute_hier_from(_compute_grad, hier, nb_ghosts=nb_ghosts)

return VectorField(h)

168 changes: 168 additions & 0 deletions pyphare/pyphare/core/ufuncs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import numpy as np

from pyphare.pharesee.hierarchy import ScalarField, VectorField

Check notice

Code scanning / CodeQL

Unused import Note

Import of 'ScalarField' is not used.
Import of 'VectorField' is not used.
from pyphare.pharesee.hierarchy.hierarchy_utils import compute_hier_from

Check notice

Code scanning / CodeQL

Unused import Note

Import of 'compute_hier_from' is not used.

from scipy.ndimage import gaussian_filter



def _compute_gaussian_filter_on_scalarfield(patch_datas, **kwargs):
from scipy.ndimage import gaussian_filter

ndim = patch_datas["value"].box.ndim
nb_ghosts = kwargs["nb_ghosts"]
sigma = kwargs["sigma"]
ds = np.asarray(patch_datas["value"][:])

ds_ = np.full(list(ds.shape), np.nan)

gf_ = gaussian_filter(ds, sigma=sigma)
select = tuple([slice(nb_ghosts, -nb_ghosts) for _ in range(ndim)])
ds_[select] = np.asarray(gf_[select])

return (
{"name": "value", "data": ds_, "centering": patch_datas["value"].centerings},
)


def _compute_gaussian_filter_on_vectorfield(patch_datas, **kwargs):
from scipy.ndimage import gaussian_filter

ref_name = next(iter(patch_datas.keys()))

ndim = patch_datas[ref_name].box.ndim
nb_ghosts = kwargs["nb_ghosts"]
sigma = kwargs["sigma"]
ds_x = np.asarray(patch_datas["x"][:])
ds_y = np.asarray(patch_datas["y"][:])
ds_z = np.asarray(patch_datas["z"][:])

dsx_ = np.full(list(ds_x.shape), np.nan)
dsy_ = np.full(list(ds_y.shape), np.nan)
dsz_ = np.full(list(ds_z.shape), np.nan)

gfx_ = gaussian_filter(ds_x, sigma=sigma)
gfy_ = gaussian_filter(ds_y, sigma=sigma)
gfz_ = gaussian_filter(ds_z, sigma=sigma)

select = tuple([slice(nb_ghosts, -nb_ghosts) for _ in range(ndim)])

dsx_[select] = np.asarray(gfx_[select])
dsy_[select] = np.asarray(gfy_[select])
dsz_[select] = np.asarray(gfz_[select])

return (
{"name": "x", "data": dsx_, "centering": patch_datas["x"].centerings},
{"name": "y", "data": dsy_, "centering": patch_datas["y"].centerings},
{"name": "z", "data": dsz_, "centering": patch_datas["z"].centerings},
)


def gFilt(hier, **kwargs):
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
sigma = kwargs.get("sigma", 2)

# time0 = list(hier.times())[0]
# level0 = 0
# p0 = 0
# pd0 = hier.levels(time0)[level0].patches[p0].patch_datas
# key0 = list(pd0.keys())[0]
# nb_ghosts = np.max(pd0[key0].ghosts_nbr)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useful?


nb_ghosts = np.max(list(hier.level(0).patches[0].patch_datas.values())[0].ghosts_nbr)

if nb_ghosts < sigma :
print("nb_ghosts ({0}) < sigma ({1}) : your gaussian filter might be dirty".format(nb_ghosts, sigma))

if hier.ndim == 1:
if isinstance(hier, ScalarField) :
h = compute_hier_from(_compute_gaussian_filter_on_scalarfield, hier, nb_ghosts=nb_ghosts, sigma=sigma)
return ScalarField(h)
elif isinstance(hier, VectorField) :
h = compute_hier_from(_compute_gaussian_filter_on_vectorfield, hier, nb_ghosts=nb_ghosts, sigma=sigma)
return VectorField(h)
else:
return NotImplemented
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
else:
return NotImplemented


def gF(hier, **kwargs):
sigma = kwargs.get("sigma", 4)
if sigma == 1:
raise ValueError("sigma value has to be at least 2")
h_ = hier.__deepcopy__(memo={})
ndim = hier.ndim
n_pad = 4*sigma+1
Comment on lines +90 to +96

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Sigma validation is incomplete and n_pad may be a float.

  1. sigma == 1 only rejects exactly 1; any value in (1, 2) passes. The check should be sigma < 2.
  2. n_pad = 4*sigma+1 — if sigma is a float (e.g. 4.5), n_pad will be a float. This propagates to np.arange (fine) but also to the slice() calls at line 132, where float indices will raise a TypeError in NumPy.
Proposed fix
 def gF(hier, **kwargs):
     sigma = kwargs.get("sigma", 4)
-    if sigma == 1:
-        raise ValueError("sigma value has to be at least 2")
+    if not np.isscalar(sigma):
+        raise TypeError("sigma must be a scalar value")
+    if sigma < 2:
+        raise ValueError("sigma value has to be at least 2")
     h_ = hier.__deepcopy__(memo={})
     ndim = hier.ndim
-    n_pad = 4*sigma+1
+    n_pad = int(4*sigma+1)
🧰 Tools
🪛 Ruff (0.15.0)

[warning] 93-93: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
In `@pyphare/pyphare/core/ufuncs.py` around lines 90 - 96, The sigma validation in
gF is too lax and n_pad can become a float; change the check from "if sigma ==
1" to "if sigma < 2" to reject values in (1,2), and ensure n_pad is an integer
by computing it as n_pad = int(4 * sigma + 1) (or validate/coerce sigma to an
integer >= 2 before use); update any uses of sigma/n_pad in gF accordingly so
slice() calls and numpy indexing receive integer indices.

# The gaussian filter is calculated on the box extended by
# n_pad. Hence, the number of points is large enough so that the value
# at the last point of the real box is as equal as possible to the one
# at the first point of the next box...

for time in h_.times():
interp_ = hier.interpol(time)
for lvl in h_.levels(time).values():
for patch in lvl.patches:
names = list(patch.patch_datas.keys())
box = patch.box

for name in names:
pdata = patch.patch_datas[name]
nb_ghosts = pdata.ghosts_nbr
if not n_pad > nb_ghosts:
raise ValueError('sigma value is too small')
Comment on lines +112 to +113

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inverted guard condition: not n_pad > nb_ghosts allows n_pad == nb_ghosts.

The check not n_pad > nb_ghosts is equivalent to n_pad <= nb_ghosts, which raises when n_pad equals nb_ghosts. Given the comment at lines 97-100 about needing padding beyond the ghost region, the intent seems to be n_pad <= nb_ghosts should be fine and only n_pad < nb_ghosts is problematic. But more importantly, nb_ghosts here is pdata.ghosts_nbr which may be an array — this comparison will produce an array, and using it in a boolean context will fail for multi-dimensional cases.

Consider clarifying the intent and using np.all() for the array comparison:

-                    if not n_pad > nb_ghosts:
-                        raise ValueError('sigma value is too small')
+                    if np.any(n_pad <= nb_ghosts):
+                        raise ValueError('sigma value is too small: n_pad must exceed nb_ghosts')
🧰 Tools
🪛 Ruff (0.15.0)

[warning] 113-113: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
In `@pyphare/pyphare/core/ufuncs.py` around lines 112 - 113, The boolean
comparison on n_pad vs nb_ghosts (where nb_ghosts is pdata.ghosts_nbr and may be
an array) is inverted and invalid for arrays; change the guard to explicitly
check elementwise and only raise when padding is strictly smaller than the ghost
region (e.g. use numpy: if np.any(n_pad < pdata.ghosts_nbr): raise
ValueError('sigma value is too small') ), referencing the variables n_pad and
pdata.ghosts_nbr so the array comparison uses np.any/np.all as appropriate for
the intended strictness.


r_ = []
for i in range(ndim):
s_ = np.arange(box.lower[i]-n_pad, box.upper[i]+2+n_pad)*pdata.dl[i]
r_.append(s_)

func, _ = interp_[name]

if ndim == 1:
data = func(r_[0])
elif ndim == 2:
data = func(r_[0], r_[1])
elif ndim == 3:
data = func(r_[0], r_[1], r_[2])
else:
raise ValueError('unvalid dimension')

Comment thread
coderabbitai[bot] marked this conversation as resolved.
gf_ = gaussian_filter(data, sigma=sigma)
select = tuple([slice(n_pad-nb_ghosts[i], -n_pad+nb_ghosts[i]) for i in range(ndim)])
pdata.dataset = np.asarray(gf_[select])

return h_

Comment on lines +90 to +136

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

scipy.ndimage.gaussian_filter sigma parameter scalar sequence documentation

💡 Result:

In scipy.ndimage.gaussian_filter(input, sigma, ...), the sigma parameter is the standard deviation of the Gaussian kernel and can be either:

  • a single scalar (same sigma used for all axes), or
  • a sequence of scalars (one sigma per axis, in axis order).

This is exactly how SciPy’s docs define it: “scalar or sequence of scalars … as a sequence [per axis], or as a single number, in which case it is equal for all axes.” [1]

Example: for a 2D array, sigma=2.0 applies (2.0, 2.0), while sigma=(1.0, 3.0) applies 1.0 on axis 0 and 3.0 on axis 1. [1]

Source: SciPy API docs for scipy.ndimage.gaussian_filter [1]

[1] https://docs.scipy.org/doc/scipy-1.11.4/reference/generated/scipy.ndimage.gaussian_filter.html


🏁 Script executed:

# Search for sigma validation patterns in the codebase
rg "sigma" pyphare/pyphare/core/ufuncs.py -B 2 -A 2

# Look for tests of the gF function
find . -name "*.py" -type f | xargs grep -l "gF" | grep -i test

Repository: PHAREHUB/PHARE

Length of output: 2272


🏁 Script executed:

# Check for any existing sigma type validation elsewhere in the codebase
rg "np.isscalar.*sigma" --type py
rg "isinstance.*sigma" --type py

Repository: PHAREHUB/PHARE

Length of output: 40


🏁 Script executed:

# Understand how numpy handles float indices in slicing
python3 << 'EOF'
import numpy as np

# Test 1: Does numpy allow float indices in slicing?
try:
    arr = np.array([1, 2, 3, 4, 5])
    result = arr[2.5:]
    print("Float index in slice: works")
except TypeError as e:
    print(f"Float index in slice: {e}")

# Test 2: What about float arithmetic in slice construction?
n_pad = 2 * 4.5  # Simulating float sigma
sigma = 4.5
try:
    print(f"n_pad (float): {n_pad}, type: {type(n_pad)}")
    ndim = 1
    select = tuple([slice(n_pad, -n_pad)])
    print(f"Slice construction with float: {select}")
    arr = np.zeros((10,))
    result = arr[select]
    print(f"Slicing with float: works, result shape: {result.shape}")
except (TypeError, ValueError) as e:
    print(f"Slicing with float: {e}")

# Test 3: Check scipy.ndimage.gaussian_filter sigma behavior
from scipy.ndimage import gaussian_filter
test_data = np.random.rand(10, 10)
try:
    result = gaussian_filter(test_data, sigma=4.5)
    print(f"gaussian_filter with float sigma: works")
except Exception as e:
    print(f"gaussian_filter with float sigma: {e}")

try:
    result = gaussian_filter(test_data, sigma=[2.0, 3.0])
    print(f"gaussian_filter with sequence sigma: works")
except Exception as e:
    print(f"gaussian_filter with sequence sigma: {e}")
EOF

Repository: PHAREHUB/PHARE

Length of output: 144


🏁 Script executed:

# Verify what happens when float values are used in slice objects with numpy arrays
python3 << 'EOF'
import numpy as np

# Test float indices in slice with numpy array
arr = np.zeros((10,))
n_pad = 4.5  # float value

try:
    select = tuple([slice(n_pad, -n_pad)])
    result = arr[select]
    print(f"Float in slice works. Result shape: {result.shape}")
    print(f"Slice object: {select}")
except TypeError as e:
    print(f"Float in slice fails: {e}")

# Test with actual usage from code
ndim = 1
nb_ghosts = np.array([1])
n_pad_float = 4.5
n_pad_int = int(np.ceil(4.5))

select_float = tuple([slice(n_pad_float-nb_ghosts[i], -n_pad_float+nb_ghosts[i]) for i in range(ndim)])
select_int = tuple([slice(n_pad_int-nb_ghosts[i], -n_pad_int+nb_ghosts[i]) for i in range(ndim)])

arr = np.zeros((15,))
try:
    result_float = arr[select_float]
    result_int = arr[select_int]
    print(f"Float slice result: {result_float.shape}")
    print(f"Int slice result: {result_int.shape}")
    print(f"Are results identical? {np.array_equal(result_float, result_int)}")
except Exception as e:
    print(f"Error: {e}")

# Test with sequence sigma
from scipy.ndimage import gaussian_filter
data = np.random.rand(10, 10)
try:
    # This should work according to scipy docs
    result = gaussian_filter(data, sigma=(2.0, 3.0))
    print(f"gaussian_filter with sequence sigma works")
    
    # But what happens if we treat it like the gF code does?
    sigma = (2.0, 3.0)
    n_pad = 2 * sigma  # This will create a tuple
    print(f"n_pad with sequence sigma: {n_pad}, type: {type(n_pad)}")
    
    # Try to use it in slicing
    ndim = 2
    nb_ghosts = np.array([1, 1])
    select = tuple([slice(n_pad[i]-nb_ghosts[i], -n_pad[i]+nb_ghosts[i]) for i in range(ndim)])
    result = data[select]
    print(f"Slicing with sequence-derived n_pad works: {result.shape}")
except Exception as e:
    print(f"Error with sequence sigma: {type(e).__name__}: {e}")
EOF

Repository: PHAREHUB/PHARE

Length of output: 144


🏁 Script executed:

# Check if there are any tests for the gF function to see expected behavior
find . -name "*.py" -type f -exec grep -l "def test.*gF\|gF.*test" {} \;

Repository: PHAREHUB/PHARE

Length of output: 40


🏁 Script executed:

# Search for actual usage of gF function in the codebase
rg "gF\(" --type py -B 2 -A 2

Repository: PHAREHUB/PHARE

Length of output: 292


🏁 Script executed:

# Check if gF is ever called with sequence sigma in tests or examples
rg "gF\s*\(" --type py -A 5 | head -50

# Look for test files that might call gF
fd "test.*\.py" --type f | head -20

Repository: PHAREHUB/PHARE

Length of output: 1424


🏁 Script executed:

# Simulate what happens with sequence sigma in the actual code path
python3 << 'EOF'
import numpy as np

# Simulate the actual code behavior with sequence sigma
sigma_sequence = (2.0, 3.0)
n_pad = 2 * sigma_sequence
print(f"n_pad with sequence sigma: {n_pad}")
print(f"Type of n_pad: {type(n_pad)}")

# Try the slice arithmetic
ndim = 2
nb_ghosts = np.array([1, 1])

try:
    select = tuple([slice(n_pad[i]-nb_ghosts[i], -n_pad[i]+nb_ghosts[i]) for i in range(ndim)])
    print(f"Slice construction works: {select}")
except (TypeError, IndexError) as e:
    print(f"Slice construction fails: {type(e).__name__}: {e}")

# Now test with float scalar sigma
sigma_float = 2.5
n_pad_float = 2 * sigma_float
print(f"\nn_pad with float scalar sigma: {n_pad_float}")
print(f"Type of n_pad: {type(n_pad_float)}")

try:
    select = tuple([slice(n_pad_float-nb_ghosts[i], -n_pad_float+nb_ghosts[i]) for i in range(ndim)])
    print(f"Slice construction works with float: {select}")
    # Test actual slicing
    data = np.zeros((10, 10))
    result = data[select]
    print(f"Actual slicing works with float indices: {result.shape}")
except (TypeError, IndexError) as e:
    print(f"Error: {type(e).__name__}: {e}")
EOF

Repository: PHAREHUB/PHARE

Length of output: 175


🏁 Script executed:

# Simple test without numpy of the sequence sigma issue
python3 << 'EOF'
# Test 1: What happens when we multiply a tuple by 2?
sigma_sequence = (2.0, 3.0)
n_pad = 2 * sigma_sequence
print(f"n_pad with sequence sigma: {n_pad}")
print(f"Type: {type(n_pad)}")

# Test 2: Can we use tuple elements in arithmetic?
try:
    result = n_pad[0] - 1
    print(f"Can index tuple n_pad: {result}")
except TypeError as e:
    print(f"Error indexing: {e}")

# Test 3: What about scalar float multiplication?
sigma_scalar = 2.5
n_pad_scalar = 2 * sigma_scalar
print(f"\nn_pad with float scalar sigma: {n_pad_scalar}")
print(f"Type: {type(n_pad_scalar)}")

# Test 4: Using float in range operations (simulating slice indices)
try:
    n_pad_float = 4.5
    idx = int(n_pad_float)
    print(f"Can convert float to int: {idx}")
    # But what about direct float use?
    print(f"Float value {n_pad_float} - int {1} = {n_pad_float - 1}, type: {type(n_pad_float - 1)}")
except Exception as e:
    print(f"Error: {e}")
EOF

Repository: PHAREHUB/PHARE

Length of output: 293


🏁 Script executed:

# Check the actual scipy.ndimage.gaussian_filter source or behavior with different sigma types
# by looking at how it's used in other parts of the codebase
rg "gaussian_filter" --type py -B 2 -A 2 | grep -E "(sigma|gaussian_filter)" | head -30

Repository: PHAREHUB/PHARE

Length of output: 2006


🏁 Script executed:

# Look at how sigma is validated in the gFilt function mentioned earlier
rg "def gFilt" --type py -A 20

Repository: PHAREHUB/PHARE

Length of output: 1585


Add validation for scalar sigma and fix padding calculation.
The sigma parameter accepts scalar or sequence values (per scipy.ndimage.gaussian_filter), but the function assumes scalar. Passing a sequence causes 2 * sigma to produce tuple concatenation, breaking slice indexing. Also, the validation sigma == 1 misses fractional values < 2.

🐛 Proposed fix
 def gF(hier, **kwargs):
-    sigma = kwargs.get("sigma", 4)
-    if sigma == 1:
-        raise ValueError("sigma value has to be at least 2")
+    sigma = kwargs.get("sigma", 4)
+    if not np.isscalar(sigma):
+        raise ValueError("sigma must be a scalar")
+    if sigma < 2:
+        raise ValueError("sigma value has to be at least 2")
     h_ = hier.__deepcopy__(memo={})
     ndim = hier.ndim
-    n_pad = 2*sigma
+    n_pad = int(np.ceil(2 * sigma))
🧰 Tools
🪛 Ruff (0.14.13)

93-93: Avoid specifying long messages outside the exception class

(TRY003)


127-127: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
In `@pyphare/pyphare/core/ufuncs.py` around lines 90 - 134, The gF function
currently assumes sigma is a scalar and computes n_pad = 2*sigma and slices with
that single value, which breaks when sigma is a sequence and also only checks
sigma == 1; modify gF to accept either a scalar or an iterable: if scalar,
promote it to a tuple of length ndim; if iterable, validate its length equals
hier.ndim and that every sigma_i >= 2 (reject values < 2); compute per-dimension
n_pad_i = int(2 * sigma_i) (ensuring integers) and use n_pad_i when building the
extended coordinate arrays (s_) and when building the select tuple for slicing
(replace single n_pad with per-dimension n_pad_i in the slice computation); keep
calls to scipy.ndimage.gaussian_filter with the original per-dimension sigma
sequence so filtering remains correct and assign pdata.dataset from gf_ using
the per-dimension select slices.


def peakIds(hier, **kwargs):
from scipy.signal import find_peaks

times = list(hier.times())
if len(times) == 1:
time = times[0]
else:
raise ValueError('multiple time is not yet implemented')

pks_ = np.array([])
hgs_ = np.array([])

names_ = kwargs.pop("names", None)
if names_ is None:
names_ = list(hier.levels(time)[0].patches[0].patch_datas.keys())

ph_ = kwargs.get('peak_heights', None)
if ph_ is None:
raise ValueError("the kwarg 'peak_heights' is mandatory for now...")

for lvl in hier.levels(time).values():
for patch in lvl.patches:
for name in names_:
pdata = patch.patch_datas[name]
ds = np.asarray(pdata.dataset)
pks = find_peaks(ds, **kwargs)
for pk, hg in zip(pks[0], pks[1]['peak_heights']):
pks_ = np.append(pks_, np.add(np.multiply(pk, patch.dl), patch.origin))
hgs_ = np.append(hgs_, hg)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

return pks_, hgs_

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

scipy.signal.find_peaks parameters height peak_heights return value

💡 Result:

scipy.signal.find_peaks(x, height=...) returns:

  • peaks: a 1D ndarray of indices of samples in x that are peaks and satisfy all conditions. [1]
  • properties: a dict of intermediate/returned peak properties. If you supplied height, it includes:
    • properties["peak_heights"]: the height (amplitude) of each returned peak, i.e., the value of x at those peak indices. [1]

About the height parameter:

  • height can be:
    • a single number (minimum height),
    • an array (per-sample constraint),
    • or a 2-element sequence (min_height, max_height) (either end can be omitted). [1]
  • If you want peak_heights computed/returned without filtering out peaks by height, pass the open interval height=(None, None). [1]

Sources: [1]


🏁 Script executed:

cat -n pyphare/pyphare/core/ufuncs.py | sed -n '136,166p'

Repository: PHAREHUB/PHARE

Length of output: 1306


Pass height parameter to find_peaks(), not peak_heights. The current code validates for peak_heights (line 152) and passes it via **kwargs to find_peaks() (line 161), which will raise TypeError since peak_heights is output-only. Filter kwargs to exclude peak_heights and validate for height instead.

🐛 Proposed fix
     names_ = kwargs.pop("names", None)
     if names_ is None:
         names_ = list(hier.levels(time)[0].patches[0].patch_datas.keys())
 
-    ph_ = kwargs.get('peak_heights', None)
-    if ph_ is None:
-        raise ValueError("the kwarg 'peak_heights' is mandatory for now...")
+    find_peaks_kwargs = {k: v for k, v in kwargs.items() if k != "peak_heights"}
+    if "height" not in find_peaks_kwargs:
+        raise ValueError("the kwarg 'height' is mandatory for now...")
 
     for lvl in hier.levels(time).values():
         for patch in lvl.patches:
             for name in names_:
                 pdata = patch.patch_datas[name]
                 ds = np.asarray(pdata.dataset)
-                pks = find_peaks(ds, **kwargs)
+                pks = find_peaks(ds, **find_peaks_kwargs)
                 for pk, hg in zip(pks[0], pks[1]['peak_heights']):
                     pks_ = np.append(pks_, np.add(np.multiply(pk, patch.dl), patch.origin))
                     hgs_ = np.append(hgs_, hg)
🧰 Tools
🪛 Ruff (0.14.13)

143-143: Avoid specifying long messages outside the exception class

(TRY003)


154-154: Avoid specifying long messages outside the exception class

(TRY003)


162-162: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

🤖 Prompt for AI Agents
In `@pyphare/pyphare/core/ufuncs.py` around lines 136 - 166, In peakIds, require
and validate the input 'height' parameter (not the output-only 'peak_heights'),
remove any 'peak_heights' key from kwargs before calling
scipy.signal.find_peaks, and pass the filtered kwargs to find_peaks;
specifically, update the validation that currently checks
kwargs.get('peak_heights') to check for 'height' instead, pop/delete
'peak_heights' from kwargs if present, then call find_peaks(ds,
**filtered_kwargs) and continue using pks[1]['peak_heights'] from the result for
peak magnitudes.

75 changes: 74 additions & 1 deletion pyphare/pyphare/pharesee/hierarchy/hierarchy.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,8 +438,9 @@
label = "L{level}P{patch}".format(level=lvl_nbr, patch=ip)
marker = kwargs.get("marker", "")
ls = kwargs.get("ls", "--")
lw = kwargs.get("lw", 1)
color = kwargs.get("color", "k")
ax.plot(x, val, label=label, marker=marker, ls=ls, color=color)
ax.plot(x, val, label=label, marker=marker, ls=ls, lw=lw, color=color)

ax.set_title(kwargs.get("title", ""))
ax.set_xlabel(kwargs.get("xlabel", "x"))
Expand Down Expand Up @@ -614,6 +615,78 @@

return final, dp(final, **kwargs)

def interpol(self, time, interp="nearest"):

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
pyphare.pharesee.hierarchy.hierarchy_utils
begins an import cycle.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^^

from pyphare.pharesee.hierarchy.hierarchy_utils import flat_finest_field
from pyphare.pharesee.run.utils import build_interpolator

nbrGhosts = list(self.level(0, time).patches[0].patch_datas.values())[0].ghosts_nbr

interp_ = {}
for qty in self.quantities():
box = self.level(0, time).patches[0].patch_datas[qty].box
dl = self.level(0, time).patches[0].patch_datas[qty].dl
data, coords = flat_finest_field(self, qty, time=time)
interp_[qty] = build_interpolator(data, coords, interp, box, dl, qty, nbrGhosts)
return interp_
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):

Check notice

Code scanning / CodeQL

Explicit returns mixed with implicit (fall through) returns Note

Mixing implicit and explicit returns may indicate an error, as implicit returns always return None.
print(f"__array_function__ of PatchHierarchy called for {ufunc.__name__}")
if method != "__call__":
return NotImplemented

# print(type(inputs), inputs, type(inputs[0]), inputs[0])

final = []

times = inputs[0].times()
for x in inputs:
assert(times == x.times())
if not isinstance(x, PatchHierarchy):
raise TypeError("this arg should be a PatchHierarchy")
ils = [key for d in inputs[0].patch_levels for key in d]
# print(ils)
h_type = type(inputs[0])

all_ = []
for i, time in enumerate(times):
# print("* * * ", i, time)
pls = []
for x in inputs:
# print(". . . ", x.times()[i], x.patch_levels[i])
pls_ = []
for ilvl, plvl in x.patch_levels[i].items():
# print("_ _ _ ", ilvl, plvl)
pls_.append(plvl)
pls.append(pls_)

out = [getattr(ufunc, method)(*pl, **kwargs) for pl in zip(*pls)]

# print(" -> ", type(out), type(out[0]))

# out est une liste de liste de patchlevel : indice sur le levels (pour 1 temps donne)
# il faut les remettre dans un dict avec ilvl

final = {}
for il, pl in zip(ils, out):
final[il] = pl
# print("___ ",final)

all_.append(final)

h_ = PatchHierarchy(all_,
domain_box=self.domain_box,
refinement_box=self.refinement_ratio,
times=self.times(),
data_files=self.data_files,
selection_box=self.selection_box)

from .scalarfield import ScalarField

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
pyphare.pharesee.hierarchy.scalarfield
begins an import cycle.
from .vectorfield import VectorField

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
pyphare.pharesee.hierarchy.vectorfield
begins an import cycle.

if h_type is ScalarField:
return ScalarField(h_)
elif h_type is VectorField:
return self

def finest_part_data(hierarchy, time=None):
"""
Expand Down
33 changes: 33 additions & 0 deletions pyphare/pyphare/pharesee/hierarchy/patch.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#
from .patchdata import FieldData

Check notice

Code scanning / CodeQL

Unused import Note

Import of 'FieldData' is not used.


class Patch:
Expand Down Expand Up @@ -67,3 +68,35 @@
return pd.dataset[idx + nbrGhosts, nbrGhosts:-nbrGhosts]
elif idim == 1:
return pd.dataset[nbrGhosts:-nbrGhosts, idx + nbrGhosts]

def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
print(f"__array_function__ of Patch called for {ufunc.__name__}")
if method != "__call__":
return NotImplemented

pds = [] # list (p1, p2, p3... ) of list ('x', 'y', 'z') of pds
for x in inputs: # inputs is a list of Patch
if isinstance(x, Patch): # hence, x is a Patch
pd_k = []
pds_ = []
for k, p in x.patch_datas.items():
pd_k.append(k)
pds_.append(p)
pds.append(pds_)
else:
raise TypeError("this arg should be a Patch")

out = [getattr(ufunc, method)(*pd, **kwargs) for pd in zip(*pds)]
# out = [ufunc(*pd, **kwargs) for pd in zip(*pds)]

final = {}
for k, pd in zip(pd_k, out): # TODO hmmmm, the output patch will keep the keys of the last patch in inputs
final[k] = pd

return Patch(final, patch_id=self.id, layout=self.layout, attrs=self.attrs)

def __array_function__(self, func, types, args, kwargs):
# TODO this has to be tested w. np.mean for example
print(f"__array_function__ of Patch {func.__name__} called for {[getattr(a, 'name', a) for a in args]}")
return func(*args, **kwargs)

Loading