Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
65 changes: 15 additions & 50 deletions pyphare/pyphare/data/wrangler.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,62 +12,27 @@ def __init__(self, simulator):
self.cpp = getattr(cpp.cpp_lib(sim), "DataWrangler")(
simulator.cpp_sim, simulator.cpp_hier
)
# todo mhd
self.modelsPerLevel = ["Hybrid" for lvl in range(self.cpp.getNumberOfLevels())]
Comment thread
PhilipDeegan marked this conversation as resolved.

def kill(self):
del self.cpp

def getNumberOfLevels(self):
return self.cpp.getNumberOfLevels()

def getPatchLevel(self, lvl):
return self.cpp.getPatchLevel(lvl)

def _lvl0FullContiguous(self, input, is_primal=True):
return self.cpp.sync_merge(input, is_primal)

def lvl0IonDensity(self):
return self._lvl0FullContiguous(self.getPatchLevel(0).getDensity())

def lvl0BulkVelocity(self):
return {
xyz: self._lvl0FullContiguous(bv)
for xyz, bv in self.getPatchLevel(0).getBulkVelocity().items()
}

def lvl0PopDensity(self):
return {
pop: self._lvl0FullContiguous(density)
for pop, density in self.getPatchLevel(0).getPopDensities().items()
}
def getMHDPatchLevel(self, lvl):
return self.cpp.getMHDPatchLevel(lvl)

def lvl0PopFluxes(self):
return {
pop: {xyz: self._lvl0FullContiguous(data) for xyz, data in flux.items()}
for pop, flux in self.getPatchLevel(0).getPopFluxes().items()
}
def getHybridPatchLevel(self, lvl):
return self.cpp.getHybridPatchLevel(lvl)

def extract_is_primal_key_from(self, em_xyz):
"""extract "Ex" from "EM_E_x" """
return "".join(em_xyz.split("_"))[2:]

def lvl0EM(self):
return {
em: {
em_xyz: self.cpp.sync_merge(
data,
gridlayout.yee_element_is_primal(
self.extract_is_primal_key_from(em_xyz)
),
)
for em_xyz, data in xyz_map.items()
}
for em, xyz_map in self.getPatchLevel(0).getEM().items()
}


# for pop, particles in dw.getPatchLevel(0).getParticles().items():
# print("pop :", pop)
# for key, patches in particles.items():
# print("\tkey :", key)
# for patch in patches:
# print("\t\t", patch.patchID, "size:", patch.data.size())
def getPatchLevel(self, lvl):
lvlModel = self.modelsPerLevel[lvl]
assert lvlModel in ["Hybrid", "MHD"]
if lvlModel == "MHD":
return self.getMHDPatchLevel(lvl)
return self.getHybridPatchLevel(lvl)

def sync(self, patch_datas):
return self.cpp.sync(patch_datas)
128 changes: 44 additions & 84 deletions pyphare/pyphare/pharesee/hierarchy/fromsim.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from pyphare import cpp
from .hierarchy_utils import isFieldQty, field_qties, quantidic, refinement_ratio
from .patchdata import FieldData, ParticleData
from .patch import Patch
Expand All @@ -10,14 +11,25 @@
import numpy as np


def hierarchy_from_sim(simulator, qty, pop=""):
def patch_gridlayout(patch, dl, interp_order):
return GridLayout(
Box(patch.lower, patch.upper), patch.origin, dl, interp_order=interp_order
)


def hierarchy_from_sim(simulator, qty, pop="", hier=None, sync=False):
"""
sync==True will copy all data to rank 0!
leaving other ranks with an empty hierarch!
"""

dw = simulator.data_wrangler()
nbr_levels = dw.getNumberOfLevels()
patch_levels = {}

root_cell_width = simulator.cell_width()
root_cell_width = np.asarray(simulator.cell_width())
domain_box = Box([0] * len(root_cell_width), simulator.domain_box())
assert len(domain_box.ndim) == len(simulator.domain_box().ndim)
assert domain_box.ndim == len(simulator.domain_box())

for ilvl in range(nbr_levels):
lvl_cell_width = root_cell_width / refinement_ratio**ilvl
Expand All @@ -26,98 +38,46 @@ def hierarchy_from_sim(simulator, qty, pop=""):
getters = quantidic(ilvl, dw)

if isFieldQty(qty):
wpatches = getters[qty]()
for patch in wpatches:
patch_datas = {}
lower = patch.lower
upper = patch.upper
origin = patch.origin
layout = GridLayout(
Box(lower, upper),
origin,
lvl_cell_width,
interp_order=simulator.interporder(),
patch_datas = getters[qty]()

if sync:
patch_datas = dw.sync(patch_datas)
if cpp.mpi_rank() > 0:
continue

for patch_data in patch_datas:
layout = patch_gridlayout(
patch_data, lvl_cell_width, simulator.interp_order()
)
patches[ilvl].append(
Patch({qty: FieldData(layout, field_qties[qty], patch_data.data)})
)
pdata = FieldData(layout, field_qties[qty], patch.data)
patch_datas[qty] = pdata
patches[ilvl].append(Patch(patch_datas))

elif qty == "particles":
elif qty == "particles": # domain only!
if sync:
raise ValueError("sync not supported for particles")
if pop == "":
raise ValueError("must specify pop argument for particles")
# here the getter returns a dict like this
# {'protons': {'patchGhost': [<pybindlibs.cpp.PatchDataContiguousParticles_1 at 0x119f78970>,
# <pybindlibs.cpp.PatchDataContiguousParticles_1 at 0x119f78f70>],
# 'domain': [<pybindlibs.cpp.PatchDataContiguousParticles_1 at 0x119f78d70>,
# <pybindlibs.cpp.PatchDataContiguousParticles_1 at 0x119f78770>]}}

# domain particles are assumed to always be here
# but patchGhost and levelGhost may not be, depending on the level

populationdict = getters[qty](pop)[pop]

dom_dw_patches = populationdict["domain"]
for patch in dom_dw_patches:
patch_datas = {}

lower = patch.lower
upper = patch.upper
origin = patch.origin
layout = GridLayout(
Box(lower, upper),
origin,
lvl_cell_width,
interp_order=simulator.interp_order(),
)
v = np.asarray(patch.data.v).reshape(int(len(patch.data.v) / 3), 3)

domain_particles = Particles(
icells=np.asarray(patch.data.iCell),
deltas=np.asarray(patch.data.delta),
v=v,
weights=np.asarray(patch.data.weight),
charges=np.asarray(patch.data.charge),

for patch_data in getters[qty](pop):
layout = patch_gridlayout(
patch_data, lvl_cell_width, simulator.interp_order()
)

patch_datas[pop + "_particles"] = ParticleData(
layout, domain_particles, pop
patches[ilvl].append( # ParticleData is SoA COPY!
Patch({qty: FieldData(layout, "tags", patch_data.data)})
)
patches[ilvl].append(Patch(patch_datas))

# ok now let's add the patchGhost if present
# note that patchGhost patches may not be the same list as the
# domain patches... since not all patches may not have patchGhost while they do have
# domain... while looping on the patchGhost items, we need to search in
# the already created patches which one to which add the patchGhost particles

for ghostParticles in ["levelGhost"]:
if ghostParticles in populationdict:
for dwpatch in populationdict[ghostParticles]:
v = np.asarray(dwpatch.data.v)
s = v.size
v = v[:].reshape(int(s / 3), 3)

patchGhost_part = Particles(
icells=np.asarray(dwpatch.data.iCell),
deltas=np.asarray(dwpatch.data.delta),
v=v,
weights=np.asarray(dwpatch.data.weight),
charges=np.asarray(dwpatch.data.charge),
)

box = Box(dwpatch.lower, dwpatch.upper)

# now search which of the already created patches has the same box
# once found we add the new particles to the ones already present

patch = [p for p in patches[ilvl] if p.box == box][0]
patch.patch_datas[pop + "_particles"].dataset.add(
patchGhost_part
)

else:
raise ValueError("{} is not a valid quantity".format(qty))

patch_levels[ilvl] = PatchLevel(ilvl, patches[ilvl])

if hier:
for lvl_nbr, level in hier.levels(hier.times()[0]).items():
new_level = patch_levels[lvl_nbr]
for ip, patch in enumerate(level.patches):
patch.patch_datas = {**patch.patch_datas, **new_level[ip].patch_datas}

return hier
return PatchHierarchy(patch_levels, domain_box, time=simulator.currentTime())
27 changes: 14 additions & 13 deletions pyphare/pyphare/pharesee/hierarchy/hierarchy_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,19 +204,20 @@ def quantidic(ilvl, wrangler):
pl = wrangler.getPatchLevel(ilvl)

return {
"density": pl.getDensity,
"bulkVelocity_x": pl.getVix,
"bulkVelocity_y": pl.getViy,
"bulkVelocity_z": pl.getViz,
"EM_B_x": pl.getBx,
"EM_B_y": pl.getBy,
"EM_B_z": pl.getBz,
"EM_E_x": pl.getEx,
"EM_E_y": pl.getEy,
"EM_E_z": pl.getEz,
"flux_x": pl.getFx,
"flux_y": pl.getFy,
"flux_z": pl.getFz,
"density": pl.getNi,
"Vi": pl.getVi,
"EM_B_x": lambda: pl.getB("x"),
"EM_B_y": lambda: pl.getB("y"),
"EM_B_z": lambda: pl.getB("z"),
"EM_E_x": lambda: pl.getE("x"),
"EM_E_y": lambda: pl.getE("y"),
"EM_E_z": lambda: pl.getE("z"),
"bulkVelocity_x": lambda: pl.getVi("x"),
"bulkVelocity_y": lambda: pl.getVi("y"),
"bulkVelocity_z": lambda: pl.getVi("z"),
"flux_x": lambda pop: pl.getFlux("x", pop),
"flux_y": lambda pop: pl.getFlux("y", pop),
"flux_z": lambda pop: pl.getFlux("z", pop),
Comment on lines +218 to +220
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

Keep the flux getters compatible with the field-quantity call path.

fromsim.py still treats flux_x/y/z as field quantities and calls getters[qty]() with no arguments. These new lambdas require pop, so any flux plot now fails with TypeError before the sync/plotting path runs. Either keep the zero-arg contract here or update the caller in the same change to pass pop for flux quantities.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pyphare/pyphare/pharesee/hierarchy/hierarchy_utils.py` around lines 218 -
220, The new flux getters break the zero-argument contract expected by
fromsim.py; change the three lambdas ("flux_x", "flux_y", "flux_z") to accept an
optional pop parameter (e.g., lambda pop=None: pl.getFlux("x", pop)) so callers
that call getters[qty]() still work while preserving the ability to pass pop
when available; update the getters definition where "flux_x"/"flux_y"/"flux_z"
are set and ensure they call pl.getFlux("x"/"y"/"z", pop).

"particles": pl.getParticles,
}

Expand Down
9 changes: 8 additions & 1 deletion pyphare/pyphare/pharesee/hierarchy/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,15 @@ def __str__(self):
def __repr__(self):
return self.__str__()

def __iter__(self):
return self.patch_datas.items().__iter__()

def __getitem__(self, key):
return self.patch_datas[key]
try:
return self.patch_datas[key]
except KeyError as e:
print(key, "not in", self.patch_datas.keys())
raise e
Comment on lines +44 to +48
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

🧩 Analysis chain

🏁 Script executed:

find . -name "patch.py" -path "*/hierarchy/*" | head -5

Repository: PHAREHUB/PHARE

Length of output: 103


🏁 Script executed:

sed -n '40,55p' ./pyphare/pyphare/pharesee/hierarchy/patch.py

Repository: PHAREHUB/PHARE

Length of output: 484


Use bare raise to preserve the original exception traceback.

In this __getitem__ method, raise e resets the traceback to line 48, which obscures where the original KeyError occurred. Use bare raise after logging to maintain the original traceback:

except KeyError as e:
    print(key, "not in", self.patch_datas.keys())
    raise
🧰 Tools
🪛 Ruff (0.15.4)

[warning] 48-48: Use raise without specifying exception name

Remove exception name

(TRY201)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pyphare/pyphare/pharesee/hierarchy/patch.py` around lines 44 - 48, In the
__getitem__ method, replace the current re-raise that uses the exception
variable (raising `e`) with a bare raise so the original traceback is preserved;
locate the except KeyError block that logs the missing key and change the
re-raise to a bare raise while keeping the print/log line that references
self.patch_datas and the key.


def copy(self):
"""does not copy patchdatas.datasets (see class PatchData)"""
Expand Down
18 changes: 15 additions & 3 deletions pyphare/pyphare/pharesee/hierarchy/patchlevel.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
#
#
#


class PatchLevel:
"""is a collection of patches"""

def __init__(self, lvl_nbr, patches):
self.level_number = lvl_nbr
self.patches = patches

def __iter__(self):
return self.patches.__iter__()

def level_range(self):
name = list(self.patches[0].patch_datas.keys())[0]
return min([patch.patch_datas[name].x.min() for patch in self.patches]), max(
[patch.patch_datas[name].x.max() for patch in self.patches]
)

def __getitem__(self, idx):
return self.patches[idx]

def __iter__(self):
return self.patches.__iter__()

@property
def cell_width(self):
return self.patches[0].layout.dl
Comment thread
PhilipDeegan marked this conversation as resolved.
7 changes: 4 additions & 3 deletions src/amr/physical_models/mhd_model.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ namespace solver
class MHDModel : public IPhysicalModel<AMR_Types>
{
public:
using patch_t = typename AMR_Types::patch_t;
using level_t = typename AMR_Types::level_t;
using Interface = IPhysicalModel<AMR_Types>;
using gridlayout_type = GridLayoutT;
using patch_t = AMR_Types::patch_t;
using level_t = AMR_Types::level_t;
using Interface = IPhysicalModel<AMR_Types>;

static constexpr std::string_view model_type_name = "MHDModel";
static inline std::string const model_name{model_type_name};
Expand Down
2 changes: 1 addition & 1 deletion src/core/utilities/mpi_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ void barrier()



std::string date_time(std::string format)
std::string date_time(std::string const& format)
{
std::time_t t = std::time(NULL);
char buffer[80];
Expand Down
Loading