Skip to content

Commit bd4116e

Browse files
committed
Merge branch 'development' of github.com:underworldcode/underworld3 into development
2 parents f141acb + 942f5f6 commit bd4116e

3 files changed

Lines changed: 189 additions & 14 deletions

File tree

src/underworld3/adaptivity.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -268,27 +268,30 @@ def mesh2mesh_swarm(mesh0, mesh1, swarm0, swarmVarList, proxy=True, verbose=Fals
268268
## some of these. So let's add them now
269269

270270
cell = mesh1.get_closest_local_cells(global_unallocated_coords)
271-
found1 = np.where(cell >= 0)[0]
272-
not_found1 = np.where(cell == -1)[0]
271+
# found1 = np.where(cell >= 0)[0]
272+
# not_found1 = np.where(cell == -1)[0]s
273+
cell_arr = np.atleast_1d(cell)
274+
found1 = np.where(cell_arr >= 0)[0]
275+
not_found1 = np.where(cell_arr == -1)[0]
273276

274277
n_found1 = found1.shape[0]
275278
n_not_found1 = not_found1.shape[0]
276279

277-
psize = swarm1.dm.getLocalSize()
278-
adds = n_found1 + 1
280+
if n_found1 > 0:
281+
psize = swarm1.dm.getLocalSize()
282+
adds = n_found1 # swarm is not blank, so only need to use N for addNPoints
279283

280-
swarm1.dm.addNPoints(adds)
284+
swarm1.dm.addNPoints(adds)
281285

282-
cellid = swarm1.dm.getField("DMSwarm_cellid")
283-
coords = swarm1.dm.getField("DMSwarmPIC_coor").reshape((-1, swarm1.dim))
286+
cellid = swarm1.dm.getField("DMSwarm_cellid")
287+
coords = swarm1.dm.getField("DMSwarmPIC_coor").reshape((-1, swarm1.dim))
284288

285-
coords[psize + 1 :, :] = global_unallocated_coords[found1, :]
289+
coords[psize + 1 :, :] = global_unallocated_coords[found1, :]
286290

287-
if n_found1 > 0:
288291
cellid[psize + 1 :] = cell[found1] ## gathered points
289292

290-
swarm1.dm.restoreField("DMSwarm_cellid")
291-
swarm1.dm.restoreField("DMSwarmPIC_coor")
293+
swarm1.dm.restoreField("DMSwarm_cellid")
294+
swarm1.dm.restoreField("DMSwarmPIC_coor")
292295

293296
# print(
294297
# f"{uw.mpi.rank}/i: {swarm1.dm.getLocalSize()} / {swarm1.dm.getSize()} cf {swarm0.dm.getSize()}",

src/underworld3/cython/petsc_generic_snes_solvers.pyx

Lines changed: 156 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,124 @@ class SolverBaseClass(uw_object):
391391
print(f"{name}.constitutive_model = uw.constitutive_models...")
392392

393393
return
394+
395+
def get_dof_partition(self,
396+
section_type: str,
397+
filename: Optional[str | None] = None,
398+
outputPath: Optional[str] = ""):
399+
"""
400+
Obtains how the degrees of freedom (DOF) are distributed/divided among the processors and saves them in an h5 file.
401+
Parameters
402+
----------
403+
section_type:
404+
Can be: "local" which includes DOFs from ghost points or "global" which differentiates DOFs from ghost points by having negative values.
405+
filename:
406+
Output file name. If None, will print out results; if set to a string, the final output file will be <filename>_<section_type>.u.h5.
407+
outputPath:
408+
Path of directory where data is saved. If left empty it will save the data in the current working directory.
409+
"""
410+
411+
self.validate_solver() # mainly check if self.u is properly set
412+
413+
u_id = self.Unknowns.u.field_id
414+
fname = None if filename is None else f"{filename}_{section_type}.u.h5"
415+
416+
self._get_dof_partition_by_field_id(section_type = section_type,
417+
field_id = u_id,
418+
filename = fname,
419+
outputPath = outputPath)
420+
421+
return
422+
423+
424+
def _get_dof_partition_by_field_id(self,
425+
section_type: str,
426+
field_id: int,
427+
filename: Optional[str | None] = None,
428+
outputPath: Optional[str] = ""):
429+
"""
430+
Private version of get_dof_partition with field_id as an additional parameter.
431+
Parameters
432+
----------
433+
section_type:
434+
Can be: "local" which includes DOFs from ghost points or "global" which differentiates DOFs from ghost points by having negative values.
435+
field_id:
436+
The field id
437+
filename:
438+
Output file name. If None, will print out results; if set to a string, resulting h5 file has the following keys: field_id, rank, dof.
439+
outputPath:
440+
Path of directory where data is saved. If left empty it will save the data in the current working directory.
441+
"""
442+
443+
import os
444+
import h5py
445+
import numpy as np
394446

447+
# check if section type is valid
448+
if section_type not in ['local', 'global']:
449+
raise("'section_type' unknown. Value must be either 'local' or 'global'")
450+
451+
# check if path exists
452+
if os.path.exists(os.path.abspath(outputPath)): # easier to debug abs
453+
pass
454+
else:
455+
raise RuntimeError(f"{os.path.abspath(outputPath)} does not exist")
456+
457+
# check if we have write access
458+
if os.access(os.path.abspath(outputPath), os.W_OK):
459+
pass
460+
else:
461+
raise RuntimeError(f"No write access to {os.path.abspath(outputPath)}")
462+
463+
464+
# get all points in the DAG of this partition
465+
if section_type == "local":
466+
section = self.mesh.dm.getLocalSection()
467+
elif section_type == "global":
468+
section = self.mesh.dm.getGlobalSection()
469+
470+
# NOTE: negative DOFs mean that these are ghost ones and owned by a different process
471+
472+
ptStart, ptEnd = section.getChart() # will give all DOFs including ghosts
473+
474+
fdofs = [section.getFieldDof(pt, field_id) for pt in range(ptStart, ptEnd)]
475+
fdofs = np.array(fdofs)
476+
pos_dof_data = np.array([field_id, uw.mpi.rank, fdofs[fdofs > 0].sum()])
477+
478+
if section_type == "global":
479+
neg_dof_data = np.array([field_id, uw.mpi.rank, fdofs[fdofs < 0].sum()])
480+
481+
comm = uw.mpi.comm
482+
483+
# Gather the arrays on rank 0
484+
gath_pos_dof_data = comm.gather(pos_dof_data, root = 0)
485+
if section_type == "global":
486+
gath_neg_dof_data = comm.gather(neg_dof_data, root = 0)
487+
488+
# pack data and save to a dataframe for formatted opening
489+
if uw.mpi.rank == 0:
490+
gath_dof_data = np.vstack(gath_pos_dof_data)
491+
if section_type == "global":
492+
gath_dof_data = np.vstack([gath_pos_dof_data, gath_neg_dof_data])
493+
494+
if filename is None: # print out
495+
print(f"Section type: {section_type}")
496+
print(f"| Field ID | Rank | # DOFs |")
497+
print(f"| ---------------------------------------------- |")
498+
for i in range(gath_dof_data.shape[0]):
499+
print(
500+
f"| {gath_dof_data[i, 0]:<15}|{gath_dof_data[i, 1]:<15}|{gath_dof_data[i, 2]:<15}|"
501+
)
502+
print(f"| ---------------------------------------------- |")
503+
print("\n", flush = True)
504+
505+
else: # save
506+
with h5py.File(f"{outputPath}/{filename}", "w") as f:
507+
f.create_dataset("field_id", data = gath_dof_data[:, 0])
508+
f.create_dataset("rank", data = gath_dof_data[:, 1])
509+
f.create_dataset("dof", data = gath_dof_data[:, 2])
510+
511+
return
395512

396513
## Specific to dimensionality
397514

@@ -2077,9 +2194,45 @@ class SNES_Stokes_SaddlePt(SolverBaseClass):
20772194
raise RuntimeError("Constitutive Model is required")
20782195

20792196
return
2080-
2081-
2082-
2197+
2198+
def get_dof_partition(self,
2199+
section_type: str,
2200+
filename: Optional[str | None] = None,
2201+
outputPath: Optional[str] = ""):
2202+
"""
2203+
Obtains how the degrees of freedom (DOF) are distributed/divided among the processors and saves them in an h5 file.
2204+
Parameters
2205+
----------
2206+
section_type:
2207+
Can be: "local" which includes DOFs from ghost points or "global" which differentiates DOFs from ghost points by having negative values.
2208+
filename:
2209+
Output file name. If None, will print out results; if set to a string, the output files will be <filename>_<section_type>.u.h5 and <filename>_<section_type>.p.h5.
2210+
outputPath:
2211+
Path of directory where data is saved. If left empty it will save the data in the current working directory.
2212+
"""
2213+
# NOTE: supposed to inherit get_dof_partition from SolverBaseClass
2214+
# NOTE: _get_dof_partition_by_field_id is defined in SolverBaseClass
2215+
2216+
self.validate_solver()
2217+
2218+
u_id = self.Unknowns.u.field_id
2219+
fname = None if filename is None else f"{filename}_{section_type}.u.h5"
2220+
2221+
self._get_dof_partition_by_field_id(section_type = section_type,
2222+
field_id = u_id,
2223+
filename = fname,
2224+
outputPath = outputPath)
2225+
2226+
p_id = self.Unknowns.p.field_id
2227+
fname = None if filename is None else f"{filename}_{section_type}.p.h5"
2228+
2229+
self._get_dof_partition_by_field_id(section_type = section_type,
2230+
field_id = p_id,
2231+
filename = fname,
2232+
outputPath = outputPath)
2233+
2234+
return
2235+
20832236
@timing.routine_timer_decorator
20842237
def _setup_pointwise_functions(self, verbose=False, debug=False, debug_name=None):
20852238
import sympy

tests/test_0002_swarm.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,22 @@ def test_create_swarmvariable(setup_data):
4141
elements = swarm.mesh._centroids.shape[0]
4242
var.save("var.h5")
4343
assert shape == (elements * 6, 2)
44+
45+
def test_addNPoints(setup_data):
46+
47+
from underworld3 import swarm
48+
49+
swarm2 = setup_data
50+
var = swarm.SwarmVariable(name = "test", swarm = swarm2, size = 1)
51+
swarm2.dm.finalizeFieldRegister()
52+
53+
swarm2.dm.addNPoints(10) # since swarm is initially empty, will add (10 - 1) points
54+
with swarm2.access():
55+
npts = swarm2.data.shape[0]
56+
assert npts == 9
57+
58+
swarm2.dm.addNPoints(1) # already has particles, so will add 1 point
59+
with swarm2.access():
60+
npts = swarm2.data.shape[0]
61+
assert npts == 10
62+

0 commit comments

Comments
 (0)