Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/flatten asktell dict values #1409

Merged
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
5acf6c7
Bump supercharge/redis-github-action from 1.7.0 to 1.8.0
dependabot[bot] Aug 14, 2024
748e16b
Fixing broken link
jmlarson1 Aug 15, 2024
aa0a976
nitpicky
jmlarson1 Aug 15, 2024
0129612
Merge pull request #1408 from Libensemble/docs/aug_15_2024
jmlarson1 Aug 15, 2024
ee2508e
initial commit, creating ask/tell gen unit test, base LibensembleGene…
jlnav Aug 16, 2024
070fc6f
add test, arrays become flattened dicts in np_to_list_dicts
jlnav Aug 16, 2024
a969f50
remove debug statement
jlnav Aug 16, 2024
6eb5fe8
additional attempts to unflatten the input dict...
jlnav Aug 16, 2024
1261274
fix index ordering, cleanup/complete tentatively unit test
jlnav Aug 19, 2024
d960b96
passthrough kwargs to superclasses, try to handle empty lists for sin…
jlnav Aug 19, 2024
3ce0ca2
better handling of multi-dim and single-dim output-array item assignm…
jlnav Aug 19, 2024
09cb4a6
comments
jlnav Aug 19, 2024
601f02c
adjust persistent_gen_wrapper, fix UniformSampleDicts
jlnav Aug 19, 2024
8b99cc1
bump pydantic versions, really trying to resolve warnings
jlnav Aug 20, 2024
b0c3107
only install balsam on pydantic 1
jlnav Aug 20, 2024
2aac3fa
various fixes to run the proxystore test on extra-ci
jlnav Aug 20, 2024
93615f5
Merge pull request #1410 from Libensemble/testing/bump_pydantic
jlnav Aug 20, 2024
d43ab66
Merge pull request #1411 from Libensemble/testing/redis_fixes
jlnav Aug 20, 2024
023ab0f
Merge pull request #1405 from Libensemble/dependabot/github_actions/d…
jmlarson1 Aug 20, 2024
6733fe5
fix ordering of parameters in implemented ask/tell classes and parent…
jlnav Aug 21, 2024
7466100
better detecting of combinable names, by stripping out the numeric su…
jlnav Aug 21, 2024
a612692
Bump globus-compute-sdk from 2.26.0 to 2.27.0
dependabot[bot] Aug 21, 2024
751de5e
deal with keys that end with integers, but aren't similar to any othe…
jlnav Aug 21, 2024
358b335
Merge pull request #1413 from Libensemble/dependabot/pip/develop/glob…
jmlarson1 Aug 22, 2024
be0af7c
Bump crate-ci/typos from 1.23.6 to 1.23.7
dependabot[bot] Aug 22, 2024
18e7079
keyword assignment of gen_specs to Surmise
jlnav Aug 22, 2024
41cedf3
Merge pull request #1415 from Libensemble/dependabot/github_actions/d…
jmlarson1 Aug 22, 2024
a34d589
forgot another keyword surmise assignment
jlnav Aug 22, 2024
60dd203
Bump crate-ci/typos from 1.23.7 to 1.24.1
dependabot[bot] Aug 23, 2024
364cf03
Merge pull request #1417 from Libensemble/dependabot/github_actions/d…
jmlarson1 Aug 26, 2024
5f33724
add unit test for awkward H and checking routine from shuds, add case…
jlnav Aug 26, 2024
41c16b7
add optional dtype argument for list_dicts_to_np to preempt "dtype di…
jlnav Aug 26, 2024
4860428
replace _to_array with list_dicts_to_np with dtype parameter. list_di…
jlnav Aug 26, 2024
ced8992
fix
jlnav Aug 26, 2024
5f6654b
better surmise link, no longer point to fork
jlnav Aug 28, 2024
cbfdf0b
Merge branch 'develop' into feature/flatten_asktell_dict_values
jlnav Aug 29, 2024
4261ca8
LibensembleGenerator can provide matching dtype for list_dicts_to_np,…
jlnav Aug 29, 2024
460bbe3
fix
jlnav Aug 29, 2024
7fdd8a6
ahhhh, just gen_specs['out']'s dtype isn't sufficient. persis_in, des…
jlnav Aug 29, 2024
69b0584
removing hardcoded gen_specs.out, removing hardcoded persis_info.nwor…
jlnav Sep 12, 2024
8c01ca9
clarify a comment
jlnav Sep 12, 2024
4541d8a
as discussed, currently gen_specs['out'] must be provided to a gen in…
jlnav Sep 12, 2024
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
4 changes: 1 addition & 3 deletions libensemble/gen_classes/aposmm.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ def __init__(
from libensemble.gen_funcs.persistent_aposmm import aposmm

gen_specs["gen_f"] = aposmm
if len(kwargs) > 0: # so user can specify aposmm-specific parameters as kwargs to constructor
gen_specs["user"] = kwargs
if not gen_specs.get("out"): # gen_specs never especially changes for aposmm even as the problem varies
n = len(kwargs["lb"]) or len(kwargs["ub"])
gen_specs["out"] = [
Expand All @@ -34,7 +32,7 @@ def __init__(
if not persis_info:
persis_info = add_unique_random_streams({}, 4, seed=4321)[1]
persis_info["nworkers"] = 4
super().__init__(gen_specs, History, persis_info, libE_info)
super().__init__(gen_specs, History, persis_info, libE_info, **kwargs)
self.all_local_minima = []
self.results_idx = 0
self.last_ask = None
Expand Down
13 changes: 6 additions & 7 deletions libensemble/gen_classes/sampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,10 @@ class UniformSample(SampleBase):
mode by adjusting the allocation function.
"""

def __init__(self, _, persis_info, gen_specs, libE_info=None):
self.persis_info = persis_info
self.gen_specs = gen_specs
self.libE_info = libE_info
def __init__(self, _=[], persis_info={}, gen_specs={}, libE_info=None, **kwargs):
super().__init__(gen_specs, _, persis_info, libE_info, **kwargs)
self._get_user_params(self.gen_specs["user"])
self.gen_specs["out"] = [("x", float, (self.n,))]
Copy link
Member

@shuds13 shuds13 Sep 12, 2024

Choose a reason for hiding this comment

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

Interesting. This would overwrite any gen_specs['out'] supplied, but I can see that declaring the input/output types should probably come with the gen. Although its possible it may support more than one type or size of float or dimensionality.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, declaring input/output types is often implied by libE's interface to be something you can do; and something we should wholeheartedly support. but for UniformSample, for example, does it create anything other than xs ?

I was thinking that it seems it doesn't matter if you decide to uniformly sample thetas instead:

gen_specs["out"] = ["theta", float, (3,)]

the fields are often nonetheless hardcoded like:

H_o["x"] = ...

Maybe that's just because its a very simple gen. But this is also true for our other gens; you really can't customize gen_specs["out"] except for sizes.

So might as well hardcode the gen's fields, I guess, and make sure its clear to a user that its really the size thats customizable.

Though I guess if fields really were configurable, we'd have a gen that resembles:

for field in gen_specs["out"]:
   if field == "theta":
      H["theta"] = do_experiment_with_theta()
   else:
      H["x"] = do_without_theta()


def ask_numpy(self, n_trials):
H_o = np.zeros(n_trials, dtype=self.gen_specs["out"])
Expand All @@ -57,11 +56,11 @@ class UniformSampleDicts(Generator):
mode by adjusting the allocation function.
"""

def __init__(self, _, persis_info, gen_specs, libE_info=None):
self.persis_info = persis_info
def __init__(self, _, persis_info, gen_specs, libE_info=None, **kwargs):
self.gen_specs = gen_specs
self.libE_info = libE_info
self.persis_info = persis_info
self._get_user_params(self.gen_specs["user"])
self.gen_specs["out"] = [("x", float, (self.n,))]
Copy link
Member

Choose a reason for hiding this comment

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

Not sure about this. We are trying to represent an external gen here. Most external gens are not going to have this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, good point; I don't think gen_specs["out"] is necessary for this specific class


def ask(self, n_trials):
H_o = []
Expand Down
10 changes: 2 additions & 8 deletions libensemble/gen_funcs/persistent_gen_wrapper.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import inspect

import numpy as np

from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG
from libensemble.tools.persistent_support import PersistentSupport
from libensemble.utils.misc import np_to_list_dicts
from libensemble.utils.misc import list_dicts_to_np, np_to_list_dicts


def persistent_gen_f(H, persis_info, gen_specs, libE_info):
Expand All @@ -24,11 +22,7 @@ def persistent_gen_f(H, persis_info, gen_specs, libE_info):
while tag not in [STOP_TAG, PERSIS_STOP]:
H_o = gen.ask(b)
if isinstance(H_o, list):
H_o_arr = np.zeros(len(H_o), dtype=gen_specs["out"])
for i in range(len(H_o)):
for key in H_o[0].keys():
H_o_arr[i][key] = H_o[i][key]
H_o = H_o_arr
H_o = list_dicts_to_np(H_o)
tag, Work, calc_in = ps.send_recv(H_o)
gen.tell(np_to_list_dicts(calc_in))

Expand Down
17 changes: 15 additions & 2 deletions libensemble/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from libensemble.comms.comms import QComm, QCommThread
from libensemble.executors import Executor
from libensemble.message_numbers import EVAL_GEN_TAG, PERSIS_STOP
from libensemble.tools.tools import add_unique_random_streams
from libensemble.utils.misc import list_dicts_to_np, np_to_list_dicts

"""
Expand Down Expand Up @@ -90,6 +91,18 @@ class LibensembleGenerator(Generator):
``ask_numpy/tell_numpy`` methods communicate numpy arrays containing the same data.
"""

def __init__(
self, gen_specs: dict = {}, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {}, **kwargs
):
self.gen_specs = gen_specs
if len(kwargs) > 0: # so user can specify gen-specific parameters as kwargs to constructor
self.gen_specs["user"] = kwargs
if not persis_info:
self.persis_info = add_unique_random_streams({}, 4, seed=4321)[1]
self.persis_info["nworkers"] = 4
Copy link
Member

Choose a reason for hiding this comment

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

whats going on here

Copy link
Member Author

Choose a reason for hiding this comment

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

The kwargs line is similar to #1307 (comment)

for persis_info, if libE ask/tell generators maintain the H, persis_info, gen_specs, libE_info, **kwargs signature, my thinking is users may prefer not needing to populate/maintain persis_info.

Its the circumstance where

aposmm = APOSMM(
    ub=1,
    lb=0,
)

is probably preferred to:

aposmm = APOSMM(
    None,
    {},
    {
        "user": {"ub": 1, "lb": 0},
    },
    {}
)

else:
self.persis_info = persis_info

@abstractmethod
def ask_numpy(self, num_points: Optional[int] = 0) -> npt.NDArray:
"""Request the next set of points to evaluate, as a NumPy array."""
Expand All @@ -113,10 +126,10 @@ class LibensembleGenThreadInterfacer(LibensembleGenerator):
"""

def __init__(
self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {}
self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {}, **kwargs
) -> None:
super().__init__(gen_specs, History, persis_info, libE_info, **kwargs)
self.gen_f = gen_specs["gen_f"]
self.gen_specs = gen_specs
self.History = History
self.persis_info = persis_info
self.libE_info = libE_info
Expand Down
42 changes: 42 additions & 0 deletions libensemble/tests/unit_tests/test_asktell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import numpy as np

from libensemble.tools.tools import add_unique_random_streams
from libensemble.utils.misc import list_dicts_to_np


def test_asktell_sampling_and_utils():
from libensemble.gen_classes.sampling import UniformSample

persis_info = add_unique_random_streams({}, 5, seed=1234)
gen_specs = {
"out": [("x", float, (2,))],
"user": {
"lb": np.array([-3, -2]),
"ub": np.array([3, 2]),
},
}

# Test initialization with libensembley parameters
gen = UniformSample(None, persis_info[1], gen_specs, None)
assert len(gen.ask(10)) == 10

# Test initialization gen-specific keyword args
gen = UniformSample(lb=np.array([-3, -2]), ub=np.array([3, 2]))
assert len(gen.ask(10)) == 10

out_np = gen.ask_numpy(3) # should get numpy arrays, non-flattened
out = gen.ask(3) # needs to get dicts, 2d+ arrays need to be flattened
assert all([len(x) == 2 for x in out]) # np_to_list_dicts is now tested

# now we test list_dicts_to_np directly
out_np = list_dicts_to_np(out)

# check combined values resemble flattened list-of-dicts values
assert out_np.dtype.names == ("x",)
for i, entry in enumerate(out):
for j, value in enumerate(entry.values()):
assert value == out_np["x"][i][j]


if __name__ == "__main__":
test_asktell_sampling_and_utils()
54 changes: 38 additions & 16 deletions libensemble/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,33 +81,51 @@ def specs_checker_setattr(obj, key, value):
obj.__dict__[key] = value


def _copy_data(array, list_dicts):
for i, entry in enumerate(list_dicts):
for field in entry.keys():
array[field][i] = entry[field]
return array
def _decide_dtype(name: str, entry, size: int) -> tuple:
if size == 1 or not size:
return (name, type(entry))
else:
return (name, type(entry), (size,))


def _decide_dtype(name, entry):
if hasattr(entry, "shape") and len(entry.shape): # numpy type
return (name, entry.dtype, entry.shape)
else:
return (name, type(entry))
def _combine_names(names: list) -> list:
"""combine fields with same name *except* for final digit"""
# how many final digits could possibly be in each name?
# do we have to iterate through negative-indexes until we reach a non-digit?
return list(set(i[:-1] if i[-1].isdigit() else i for i in names))


def list_dicts_to_np(list_dicts: list) -> npt.NDArray:
if list_dicts is None:
return None

first = list_dicts[0]
new_dtype_names = [i for i in first.keys()]
first = list_dicts[0] # for determining dtype of output np array
new_dtype_names = _combine_names([i for i in first.keys()]) # -> ['x', 'y']
combinable_names = [] # [['x0', 'x1'], ['y0', 'y1', 'y2'], []]
for name in new_dtype_names:
combinable_names.append([i for i in first.keys() if i[:-1] == name])

new_dtype = []
for i, entry in enumerate(first.values()): # must inspect values to get presumptive types

for i, entry in enumerate(combinable_names):
name = new_dtype_names[i]
new_dtype.append(_decide_dtype(name, entry))
size = len(combinable_names[i])
if len(entry): # combinable names detected, e.g. x0, x1
new_dtype.append(_decide_dtype(name, first[entry[0]], size))
else: # only a single name, e.g. local_pt
new_dtype.append(_decide_dtype(name, first[name], size))

out = np.zeros(len(list_dicts), dtype=new_dtype)
return _copy_data(out, list_dicts)

for i, group in enumerate(combinable_names):
new_dtype_name = new_dtype_names[i]
for j, input_dict in enumerate(list_dicts):
if not len(group): # only a single name, e.g. local_pt
out[new_dtype_name][j] = input_dict[new_dtype_name]
else: # combinable names detected, e.g. x0, x1
out[new_dtype_name][j] = tuple([input_dict[name] for name in group])

return out


def np_to_list_dicts(array: npt.NDArray) -> List[dict]:
Expand All @@ -117,6 +135,10 @@ def np_to_list_dicts(array: npt.NDArray) -> List[dict]:
for row in array:
new_dict = {}
for field in row.dtype.names:
new_dict[field] = row[field]
if hasattr(row[field], "__len__") and len(row[field]) > 1:
for i, x in enumerate(row[field]):
new_dict[field + str(i)] = x
else:
new_dict[field] = row[field]
out.append(new_dict)
return out
Loading