Skip to content

Commit

Permalink
Merge pull request #4878 from BrianMarre/topic-customInputToPyPIConGPU
Browse files Browse the repository at this point in the history
custom user input in PyPIConGPU
  • Loading branch information
psychocoderHPC committed May 7, 2024
2 parents 6d5de72 + 3acf2c9 commit 2f4c338
Show file tree
Hide file tree
Showing 12 changed files with 344 additions and 11 deletions.
3 changes: 3 additions & 0 deletions lib/python/picongpu/picmi/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,9 @@ def get_as_pypicongpu(self) -> simulation.Simulation:
# explictly disable laser (as required by pypicongpu)
s.laser = None

# custom user input must always be set by the user on PyPIConGPU level
s.custom_user_input = None

# resolve electrons
self.__resolve_electrons()

Expand Down
2 changes: 2 additions & 0 deletions lib/python/picongpu/pypicongpu/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from . import species
from . import util
from . import output
from . import customuserinput

__all__ = [
"Simulation",
Expand All @@ -23,6 +24,7 @@
"species",
"util",
"grid",
"customuserinput",
]

# note: put down here b/c linter complains if imports are not at top
Expand Down
62 changes: 62 additions & 0 deletions lib/python/picongpu/pypicongpu/customuserinput.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""
This file is part of the PIConGPU.
Copyright 2024 PIConGPU contributors
Authors: Brian Edward Marre
License: GPLv3+
"""

from .rendering import RenderedObject

import typeguard
import typing


class CustomUserInput(RenderedObject):
"""
container for easy passing of additional input as dict from user script to rendering context of simulation input
if additional
"""

tags: typing.Optional[list[str]] = None
"""
list of tags
"""

rendering_context: typing.Optional[dict[str, typing.Any]] = None
"""
accumulation variable of added dictionaries
"""

def __checkDoesNotChangeExistingKeyValues(self, firstDict, secondDict):
for key in firstDict.keys():
if (key in secondDict) and (firstDict[key] != secondDict[key]):
raise ValueError("Key " + str(key) + " exist already, and specified values differ.")

@typeguard.typechecked
def addToCustomInput(self, custom_input: dict[str, typing.Any], tag: str):
"""
append dictionary to custom input dictionary
"""
if tag == "":
raise ValueError("tag must not be empty string!")
if not custom_input:
raise ValueError("custom input must contain at least 1 key")

if (self.tags is None) and (self.rendering_context is None):
self.tags = [tag]
self.rendering_context = custom_input
else:
self.__checkDoesNotChangeExistingKeyValues(self.rendering_context, custom_input)

if tag in self.tags:
raise ValueError("duplicate tag!")

self.rendering_context.update(custom_input)
self.tags.append(tag)

def get_tags(self) -> list[str]:
return self.tags

def _get_serialized(self) -> dict:
return self.rendering_context
54 changes: 54 additions & 0 deletions lib/python/picongpu/pypicongpu/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ class Simulation(RenderedObject):
used for normalization of units
"""

# may not use util.build_typesafe_property since this attribute is usually never initialized
custom_user_input = util.build_typesafe_property(typing.Optional[list[RenderedObject]])
"""
object that contains additional user specified input parameters to be used in custom templates
@attention custom user input is global to the simulation
"""

def __get_output_context(self) -> dict:
"""retrieve all output objects"""
auto = output.Auto()
Expand All @@ -62,6 +70,46 @@ def __get_output_context(self) -> dict:
"auto": auto.get_rendering_context(),
}

def __checkDoesNotChangeExistingKeyValues(self, firstDict, secondDict):
for key in firstDict.keys():
if (key in secondDict) and (firstDict[key] != secondDict[key]):
raise ValueError("Key " + str(key) + " exist already, and specified values differ.")

def __checkTags(self, existing_tags, tags):
if "" in tags:
raise ValueError("tags must not be empty string!")
for tag in tags:
if tag in existing_tags:
raise ValueError("duplicate tag provided!, tags must be unique!")

def __render_custom_user_input_list(self) -> dict:
custom_rendering_context = {"tags": []}

for entry in self.custom_user_input:
add_context = entry.get_rendering_context()
tags = entry.get_tags()

self.__checkDoesNotChangeExistingKeyValues(custom_rendering_context, add_context)
self.__checkTags(custom_rendering_context["tags"], tags)

custom_rendering_context.update(add_context)
custom_rendering_context["tags"].extend(tags)

return custom_rendering_context

def __foundCustomInput(self, serialized: dict):
print("NOTE: found custom user input with tags: " + str(serialized["customuserinput"]["tags"]))
print(
"\t WARNING: custom input is not checked, it is the users responsibility to check inputs and generated input."
)
print("\t WARNING: custom templates are required if using custom user input.")

def add_custom_user_input(self, custom_input: RenderedObject):
if self.custom_user_input is None:
self.custom_user_input = [custom_input]
else:
self.custom_user_input.append(custom_input)

def _get_serialized(self) -> dict:
serialized = {
"delta_t_si": self.delta_t_si,
Expand All @@ -78,4 +126,10 @@ def _get_serialized(self) -> dict:
else:
serialized["laser"] = None

if self.custom_user_input is not None:
serialized["customuserinput"] = self.__render_custom_user_input_list()
self.__foundCustomInput(serialized)
else:
serialized["customuserinput"] = None

return serialized
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.customrenderingcontext.CustomRenderingContext",
"description": "additional input provided by user directly to the PyPIConGPU simulation object for use in custom templates",
"type": "object",
"properties": {
"tags":{
"description": "list of unique strings identifying the version/content of the custom input",
"type": "array",
"items": {
"type": "string"
},
"minItems": 1
}
},
"required": [
"tags"
],
"unevaluatedProperties": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$id": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.customuserinput.CustomUserInput",
"description": "container class for passing dictionaries from the user script to the PyPIConGPU renderer",
"type": "object",
"unevaluatedProperties": true
}
12 changes: 11 additions & 1 deletion share/picongpu/pypicongpu/schema/simulation.Simulation.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@
]
}
}
},
"customuserinput":{
"anyOf": [
{
"type": "null"
},
{
"$ref": "https://registry.hzdr.de/crp/picongpu/schema/picongpu.pypicongpu.customrenderingcontext.CustomRenderingContext"
}]
}
},
"required": [
Expand All @@ -61,7 +70,8 @@
"typical_ppc",
"solver",
"grid",
"laser"
"laser",
"customuserinput"
],
"unevaluatedProperties": false
}
1 change: 1 addition & 0 deletions test/python/picongpu/compiling/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def test_minimal(self):
sim.grid.boundary_condition_y = pypicongpu.grid.BoundaryCondition.PERIODIC
sim.grid.boundary_condition_z = pypicongpu.grid.BoundaryCondition.PERIODIC
sim.laser = None
sim.custom_user_input = None
sim.solver = pypicongpu.solver.YeeSolver()
sim.init_manager = pypicongpu.species.InitManager()

Expand Down
58 changes: 49 additions & 9 deletions test/python/picongpu/quick/picmi/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def test_cfl_yee(self):

def test_cfl_not_yee(self):
# if the solver is not yee, cfl and timestep can be set however
# -> none of this raises an erorr
# -> none of this raises an error
get_sim_cfl_helper(7.14500557764070900528e-9, 0.99, (3, 4, 5), "CKC")
get_sim_cfl_helper(42, 0.99, (3, 4, 5), "CKC")
get_sim_cfl_helper(None, 0.99, (3, 4, 5), "CKC")
Expand All @@ -128,28 +128,68 @@ def test_species_translation(self):
sim = picmi.Simulation(time_step_size=17, max_steps=4, solver=solver)

profile = picmi.UniformDistribution(density=42)
layout = picmi.PseudoRandomLayout(n_macroparticles_per_cell=3)
layout3 = picmi.PseudoRandomLayout(n_macroparticles_per_cell=3)
layout4 = picmi.PseudoRandomLayout(n_macroparticles_per_cell=4)

# species list empty by default
self.assertEqual([], sim.get_as_pypicongpu().init_manager.all_species)

# not placed
sim.add_species(picmi.Species(name="dummy1", mass=5), None)

# placed with entire placement
sim.add_species(
picmi.Species(name="dummy2", mass=3, density_scale=4, initial_distribution=profile),
layout,
)
# placed with entire placement and 3ppc
sim.add_species(picmi.Species(name="dummy2", mass=3, density_scale=4, initial_distribution=profile), layout3)

# placed with default ratio of 1
sim.add_species(picmi.Species(name="dummy3", mass=3, initial_distribution=profile), layout)
# placed with default ratio of 1 and 4ppc
sim.add_species(picmi.Species(name="dummy3", mass=3, initial_distribution=profile), layout4)

picongpu = sim.get_as_pypicongpu()
self.assertEqual(3, len(picongpu.init_manager.all_species))
species_names = set(map(lambda species: species.name, picongpu.init_manager.all_species))
self.assertEqual({"dummy1", "dummy2", "dummy3"}, species_names)

# check typical ppc is derived
self.assertEqual(picongpu.typical_ppc, 2)

def test_explicit_typical_ppc(self):
grid = get_grid(1, 1, 1, 64)
solver = picmi.ElectromagneticSolver(method="Yee", grid=grid)
sim = picmi.Simulation(time_step_size=17, max_steps=4, solver=solver, picongpu_typical_ppc=15)

profile = picmi.UniformDistribution(density=42)
layout3 = picmi.PseudoRandomLayout(n_macroparticles_per_cell=3)
layout4 = picmi.PseudoRandomLayout(n_macroparticles_per_cell=4)

# placed with entire placement and 3ppc
sim.add_species(picmi.Species(name="dummy2", mass=3, density_scale=4, initial_distribution=profile), layout3)
# placed with default ratio of 1 and 4ppc
sim.add_species(picmi.Species(name="dummy3", mass=3, initial_distribution=profile), layout4)

picongpu = sim.get_as_pypicongpu()
self.assertEqual(2, len(picongpu.init_manager.all_species))
species_names = set(map(lambda species: species.name, picongpu.init_manager.all_species))
self.assertEqual({"dummy2", "dummy3"}, species_names)

# check explicitly set typical ppc is respected
self.assertEqual(picongpu.typical_ppc, 15)

def test_wrong_explicitly_set_typical_ppc(self):
grid = get_grid(1, 1, 1, 64)
solver = picmi.ElectromagneticSolver(method="Yee", grid=grid)

wrongValues = [0, -1, -15]
for value in wrongValues:
sim = picmi.Simulation(time_step_size=17, max_steps=4, solver=solver, picongpu_typical_ppc=value)
with self.assertRaisesRegex(ValueError, "typical_ppc must be >= 1"):
sim.get_as_pypicongpu()

wrongTypes = [0.0, -1.0, -15.0, 1.0, 15.0]
for value in wrongTypes:
with self.assertRaisesRegex(
typeguard.TypeCheckError, '"picongpu_typical_ppc" .* did not match any element in the union'
):
sim = picmi.Simulation(time_step_size=17, max_steps=4, solver=solver, picongpu_typical_ppc=value)

def test_invalid_placement(self):
profile = picmi.UniformDistribution(density=42)
layout = picmi.PseudoRandomLayout(n_macroparticles_per_cell=3)
Expand Down
1 change: 1 addition & 0 deletions test/python/picongpu/quick/pypicongpu/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
from .output import * # pyflakes.ignore
from .rendering import * # pyflakes.ignore
from .laser import * # pyflakes.ignore
from .customuserinput import * # pyflakes.ignore
70 changes: 70 additions & 0 deletions test/python/picongpu/quick/pypicongpu/customuserinput.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""
This file is part of the PIConGPU.
Copyright 2024 PIConGPU contributors
Authors: Brian Edward Marre
License: GPLv3+
"""

from picongpu.pypicongpu import customuserinput

import unittest


class TestCustomUserInput(unittest.TestCase):
# test standard workflow is possible and data+tag is passed on
def test_standard_case_works(self):
c = customuserinput.CustomUserInput()
data1 = {"test_data_1": 1}
data2 = {"test_data_2": 2}

tag1 = "tag_1"
tag2 = "tag_2"

c.addToCustomInput(data1, tag1)
c.addToCustomInput(data2, tag2)

rendering_context = c.get_rendering_context()

self.assertEqual(rendering_context["test_data_1"], 1)
self.assertEqual(rendering_context["test_data_2"], 2)

tags = c.get_tags()
self.assertIn(tag1, tags)
self.assertIn(tag2, tags)

def test_wrong_tags(self):
c = customuserinput.CustomUserInput()

data1 = {"test_data_1": 1}
data2 = {"test_data_2": 2}

tag1_1 = "tag_1"
tag1_2 = "tag_1"

# first add must succeed
c.addToCustomInput(data1, tag1_1)
with self.assertRaisesRegex(ValueError, "duplicate tag!"):
c.addToCustomInput(data2, tag1_2)

with self.assertRaisesRegex(ValueError, "tag must not be empty"):
c.addToCustomInput(data2, "")

def test_wrong_custom_input(self):
c = customuserinput.CustomUserInput()

data1_1 = {"test_data_1": 1}
data1_2 = {"test_data_1": 2}
empty_data = {}

tag1 = "tag_1"
tag2 = "tag_2"

with self.assertRaisesRegex(ValueError, "custom input must contain at least 1 key"):
c.addToCustomInput(empty_data, tag1)

c.addToCustomInput(data1_1, tag1)
with self.assertRaisesRegex(ValueError, "Key test_data_1 exist already, and specified values differ."):
c.addToCustomInput(data1_2, tag2)

# test same key with same value is allowed
c.addToCustomInput(data1_1, tag2)

0 comments on commit 2f4c338

Please sign in to comment.