From 339b4654da09a823f76b20ac9434df85e23a1ebe Mon Sep 17 00:00:00 2001 From: "Christopher M. Pierce" Date: Mon, 3 Aug 2026 14:42:03 -0700 Subject: [PATCH 1/8] add data_dump_file; rename dump_file -> xopt_dump_file --- xopt/base.py | 86 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 63 insertions(+), 23 deletions(-) diff --git a/xopt/base.py b/xopt/base.py index 0e1bc03bb..b86dfcb6a 100644 --- a/xopt/base.py +++ b/xopt/base.py @@ -40,6 +40,11 @@ logger = logging.getLogger(__name__) +DUMP_FILE_RENAME_MESSAGE = ( + "The attribute `dump_file` in `Xopt` has been renamed to `xopt_dump_file`. " + "Support for the old name will be removed in later versions of the Xopt library." +) + class Xopt(XoptBaseModel): """ @@ -59,9 +64,11 @@ class Xopt(XoptBaseModel): strict : bool, optional A flag indicating whether exceptions raised during evaluation should stop the optimization process. - dump_file : str, optional + xopt_dump_file : str, optional An optional file path for dumping attributes of the xopt object and the results of evaluations. + data_dump_file : str, optional + An optional file path for dumping the evaluation data as a CSV file. data : DataFrame, optional An optional DataFrame object for storing internal data related to the optimization process. @@ -96,8 +103,8 @@ class Xopt(XoptBaseModel): Xopt. yaml(**kwargs) Serializes the Xopt configuration to a YAML string. - dump(file: str = None, **kwargs) - Dumps the Xopt configuration to a specified file. + dump(file: str = None, data_file: str = None, **kwargs) + Dumps the Xopt configuration and/or the evaluation data to the specified files. dict(**kwargs) -> dict Provides a custom dictionary representation of the Xopt configuration. json(**kwargs) -> str @@ -115,8 +122,11 @@ class Xopt(XoptBaseModel): description="flag to indicate if exceptions raised during evaluation " "should stop Xopt", ) - dump_file: Optional[str] = Field( - None, description="file to dump the results of the evaluations" + xopt_dump_file: Optional[str] = Field( + None, description="file to dump the serialized Xopt object to" + ) + data_dump_file: Optional[str] = Field( + None, description="file to dump the evaluation data to as CSV" ) data: Optional[DataFrame] = Field(None, description="internal DataFrame object") serialize_torch: bool = Field( @@ -243,6 +253,33 @@ def max_evaluations_legacy(cls, data: Any): ) return data + @model_validator(mode="before") + @classmethod + def dump_file_legacy(cls, data: Any): + """ + Handle backward compatibility: convert the old dump_file parameter to + xopt_dump_file. + """ + if isinstance(data, dict) and "dump_file" in data: + warnings.warn(DUMP_FILE_RENAME_MESSAGE, DeprecationWarning, stacklevel=2) + if data.get("xopt_dump_file") is not None: + raise ValueError( + "Cannot specify both 'dump_file' and 'xopt_dump_file'. " + "Use 'xopt_dump_file' instead." + ) + data["xopt_dump_file"] = data.pop("dump_file") + return data + + @property + def dump_file(self) -> Optional[str]: + warnings.warn(DUMP_FILE_RENAME_MESSAGE, DeprecationWarning, stacklevel=2) + return self.xopt_dump_file + + @dump_file.setter + def dump_file(self, value: Optional[str]): + warnings.warn(DUMP_FILE_RENAME_MESSAGE, DeprecationWarning, stacklevel=2) + self.xopt_dump_file = value + @property def n_data(self) -> int: if self.data is None: @@ -440,9 +477,8 @@ def evaluate_data( self.add_data(output_data) - # dump data to file if specified - if self.dump_file is not None: - self.dump() + # dump to file(s) if specified + self.dump() return output_data @@ -622,34 +658,38 @@ def yaml(self, **kwargs): ) return yaml.dump(output) - def dump(self, file: str = None, **kwargs): + def dump(self, file: str = None, data_file: str = None, **kwargs): """ - Dump data to a file. + Dump the Xopt object and/or the evaluation data to file. + + Each target is written only if a path is available for it, either via argument + or via the corresponding attribute. If neither is available nothing is written. Parameters ---------- file : str, optional - The path to the file where the Xopt configuration will be dumped. + The path to the YAML file where the Xopt configuration will be dumped. + Defaults to the `xopt_dump_file` attribute. + data_file : str, optional + The path to the CSV file where the evaluation data will be dumped. + Defaults to the `data_dump_file` attribute. **kwargs - Additional keyword arguments for customizing the dump. - - Raises - ------ - ValueError - If no dump file is specified via argument or in the `dump_file` attribute. + Additional keyword arguments for customizing the YAML serialization. """ - fname = file if file is not None else self.dump_file + fname = file if file is not None else self.xopt_dump_file + data_fname = data_file if data_file is not None else self.data_dump_file - if fname is None: - raise ValueError( - "no dump file specified via argument or in `dump_file` attribute" - ) - else: + if fname is not None: with open(fname, "w") as f: f.write(self.yaml(**kwargs)) logger.debug(f"Dumped state to YAML file: {fname}") + if data_fname is not None: + data = self.data if self.data is not None else pd.DataFrame() + data.to_csv(data_fname, index_label="xopt_index") + logger.debug(f"Dumped data to CSV file: {data_fname}") + def dict(self, **kwargs) -> dict: """ Handle custom dictionary generation. From 24f77b6392d0123bd7368eb28a69537bc118bf05 Mon Sep 17 00:00:00 2001 From: "Christopher M. Pierce" Date: Mon, 3 Aug 2026 14:42:13 -0700 Subject: [PATCH 2/8] update and add tests --- .../generators/bayesian/test_high_level.py | 6 +- xopt/tests/generators/bayesian/test_turbo.py | 2 +- xopt/tests/generators/ga/test_cnsga.py | 4 +- xopt/tests/test_xopt.py | 82 +++++++++++++++++-- 4 files changed, 82 insertions(+), 12 deletions(-) diff --git a/xopt/tests/generators/bayesian/test_high_level.py b/xopt/tests/generators/bayesian/test_high_level.py index 617b9b775..0ceb52446 100644 --- a/xopt/tests/generators/bayesian/test_high_level.py +++ b/xopt/tests/generators/bayesian/test_high_level.py @@ -90,7 +90,7 @@ def test_mobo(self): def test_restart_torch_inline_serialization(self): YAML = """ - dump_file: dump_inline.yml + xopt_dump_file: dump_inline.yml serialize_torch: True serialize_inline: True @@ -136,7 +136,7 @@ def test_restart_torch_inline_serialization(self): def test_restart_torch_serialization(self): YAML = """ - dump_file: dump.yml + xopt_dump_file: dump.yml serialize_torch: True generator: @@ -179,7 +179,7 @@ def test_restart_torch_serialization(self): def test_restart(self): YAML = """ - dump_file: dump.yml + xopt_dump_file: dump.yml generator: name: mobo reference_point: {y1: 1.5, y2: 1.5} diff --git a/xopt/tests/generators/bayesian/test_turbo.py b/xopt/tests/generators/bayesian/test_turbo.py index ee0208e3e..82bc3c6ef 100644 --- a/xopt/tests/generators/bayesian/test_turbo.py +++ b/xopt/tests/generators/bayesian/test_turbo.py @@ -489,7 +489,7 @@ def test_serialization(self): X = Xopt( evaluator=evaluator, generator=generator, - dump_file="dump.yml", + xopt_dump_file="dump.yml", ) yaml_str = X.yaml() diff --git a/xopt/tests/generators/ga/test_cnsga.py b/xopt/tests/generators/ga/test_cnsga.py index aea06c678..e58611bbb 100644 --- a/xopt/tests/generators/ga/test_cnsga.py +++ b/xopt/tests/generators/ga/test_cnsga.py @@ -83,7 +83,7 @@ def test_cnsga_from_yaml(): stopping_condition: name: MaxEvaluationsCondition max_evaluations: 10 - dump_file: null + xopt_dump_file: null data: null generator: name: cnsga @@ -120,7 +120,7 @@ def test_cnsga_no_constraints(): stopping_condition: name: MaxEvaluationsCondition max_evaluations: 10 - dump_file: null + xopt_dump_file: null data: null generator: name: cnsga diff --git a/xopt/tests/test_xopt.py b/xopt/tests/test_xopt.py index 0bdf43e14..87f8762d5 100644 --- a/xopt/tests/test_xopt.py +++ b/xopt/tests/test_xopt.py @@ -54,7 +54,7 @@ def dummy(x): # init with yaml YAML = """ - dump_file: null + xopt_dump_file: null data: null evaluator: function: xopt.resources.test_functions.tnk.evaluate_TNK @@ -107,7 +107,7 @@ def test_legacy_vocs_yaml(self): """ # init with yaml YAML = """ - dump_file: null + xopt_dump_file: null data: null evaluator: function: xopt.resources.test_functions.tnk.evaluate_TNK @@ -151,7 +151,7 @@ def test_legacy_vocs_duplicate_yaml(self): """ # init with yaml YAML = """ - dump_file: null + xopt_dump_file: null data: null evaluator: function: xopt.resources.test_functions.tnk.evaluate_TNK @@ -492,7 +492,7 @@ def test_dump_w_exploded_cols(self): generator=generator, evaluator=evaluator, ) - X.dump_file = "test_checkpointing.yaml" + X.xopt_dump_file = "test_checkpointing.yaml" # test case with exploded data data = pd.DataFrame( @@ -525,7 +525,7 @@ def test_checkpointing(self): generator=generator, evaluator=evaluator, ) - X.dump_file = "test_checkpointing.yaml" + X.xopt_dump_file = "test_checkpointing.yaml" X.step() @@ -533,13 +533,83 @@ def test_checkpointing(self): X.step() # try to load the state from nothing - X2 = Xopt.from_file(X.dump_file) + X2 = Xopt.from_file(X.xopt_dump_file) assert len(X2.data) == 6 assert isinstance(X2.generator, RandomGenerator) assert isinstance(X2.evaluator, Evaluator) assert X.vocs == X2.vocs + def test_dump_file_legacy_name(self): + evaluator = Evaluator(function=xtest_callable) + generator = RandomGenerator(vocs=deepcopy(TEST_VOCS_BASE)) + + with pytest.warns(DeprecationWarning, match="renamed to `xopt_dump_file`"): + X = Xopt( + generator=generator, + evaluator=evaluator, + dump_file="test_checkpointing.yaml", + ) + assert X.xopt_dump_file == "test_checkpointing.yaml" + + YAML = """ + dump_file: test_checkpointing.yaml + evaluator: + function: xopt.resources.test_functions.tnk.evaluate_TNK + + generator: + name: random + vocs: + variables: + x1: [0, 3.14159] + x2: [0, 3.14159] + objectives: {y1: MINIMIZE} + """ + with pytest.warns(DeprecationWarning, match="renamed to `xopt_dump_file`"): + X = Xopt(YAML) + assert X.xopt_dump_file == "test_checkpointing.yaml" + + def test_dump_file_legacy_conflict(self): + evaluator = Evaluator(function=xtest_callable) + generator = RandomGenerator(vocs=deepcopy(TEST_VOCS_BASE)) + + with pytest.raises(ValidationError): + Xopt( + generator=generator, + evaluator=evaluator, + dump_file="legacy.yaml", + xopt_dump_file="test_checkpointing.yaml", + ) + + def test_data_dump_file(self, tmp_path): + evaluator = Evaluator(function=xtest_callable) + generator = RandomGenerator(vocs=deepcopy(TEST_VOCS_BASE)) + + data_dump_file = str(tmp_path / "data.csv") + X = Xopt( + generator=generator, + evaluator=evaluator, + data_dump_file=data_dump_file, + ) + X.random_evaluate(3) + + assert os.path.exists(data_dump_file) + dumped_data = pd.read_csv(data_dump_file, index_col="xopt_index") + pd.testing.assert_frame_equal( + dumped_data, X.data, check_dtype=False, check_names=False + ) + + def test_dump_without_files(self, tmp_path): + evaluator = Evaluator(function=xtest_callable) + generator = RandomGenerator(vocs=deepcopy(TEST_VOCS_BASE)) + + X = Xopt(generator=generator, evaluator=evaluator) + X.random_evaluate(1) + + # no dump files specified, nothing should be written and no error raised + X.dump() + assert not list(tmp_path.iterdir()) + def test_random_evaluate(self): evaluator = Evaluator(function=xtest_callable) generator = RandomGenerator(vocs=deepcopy(TEST_VOCS_BASE)) From 29d56a04140df42eef366b2d601737f97f95187f Mon Sep 17 00:00:00 2001 From: "Christopher M. Pierce" Date: Mon, 3 Aug 2026 14:42:19 -0700 Subject: [PATCH 3/8] add documentation --- docs/examples/basic/checkpointing_and_restarts.ipynb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/examples/basic/checkpointing_and_restarts.ipynb b/docs/examples/basic/checkpointing_and_restarts.ipynb index 30750324f..b122b40b1 100644 --- a/docs/examples/basic/checkpointing_and_restarts.ipynb +++ b/docs/examples/basic/checkpointing_and_restarts.ipynb @@ -10,8 +10,10 @@ }, "source": [ "# Checkpointing and Restarts\n", - "If `dump_file` is provided Xopt will save the data and the Xopt configuration in a\n", - "yaml file. This can be used directly to create a new Xopt object." + "If `xopt_dump_file` is provided Xopt will save the data and the Xopt configuration\n", + "in a yaml file. This can be used directly to create a new Xopt object. If\n", + "`data_dump_file` is provided Xopt will additionally write the evaluation data to a\n", + "csv file." ] }, { @@ -30,7 +32,7 @@ "\n", "# Make a proper input file.\n", "YAML = \"\"\"\n", - "dump_file: dump.yml\n", + "xopt_dump_file: dump.yml\n", "generator:\n", " name: random\n", " vocs:\n", From 5d83756f00f15b6b569b2b7274f7e5f0689cf65a Mon Sep 17 00:00:00 2001 From: "Christopher M. Pierce" Date: Mon, 3 Aug 2026 14:46:06 -0700 Subject: [PATCH 4/8] expand environment variables on dump --- xopt/base.py | 8 +++++++- xopt/tests/test_xopt.py | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/xopt/base.py b/xopt/base.py index b86dfcb6a..214d43c27 100644 --- a/xopt/base.py +++ b/xopt/base.py @@ -1,5 +1,6 @@ import json import logging +import os from copy import deepcopy from typing import Any, Optional, Union @@ -66,9 +67,10 @@ class Xopt(XoptBaseModel): optimization process. xopt_dump_file : str, optional An optional file path for dumping attributes of the xopt object and the - results of evaluations. + results of evaluations. Environment variables are expanded when dumping. data_dump_file : str, optional An optional file path for dumping the evaluation data as a CSV file. + Environment variables are expanded when dumping. data : DataFrame, optional An optional DataFrame object for storing internal data related to the optimization process. @@ -664,6 +666,8 @@ def dump(self, file: str = None, data_file: str = None, **kwargs): Each target is written only if a path is available for it, either via argument or via the corresponding attribute. If neither is available nothing is written. + Environment variables in the paths are expanded here, so the unexpanded paths + are the ones stored on the object and written to the serialized configuration. Parameters ---------- @@ -681,11 +685,13 @@ def dump(self, file: str = None, data_file: str = None, **kwargs): data_fname = data_file if data_file is not None else self.data_dump_file if fname is not None: + fname = os.path.expandvars(fname) with open(fname, "w") as f: f.write(self.yaml(**kwargs)) logger.debug(f"Dumped state to YAML file: {fname}") if data_fname is not None: + data_fname = os.path.expandvars(data_fname) data = self.data if self.data is not None else pd.DataFrame() data.to_csv(data_fname, index_label="xopt_index") logger.debug(f"Dumped data to CSV file: {data_fname}") diff --git a/xopt/tests/test_xopt.py b/xopt/tests/test_xopt.py index 87f8762d5..6d6c9a235 100644 --- a/xopt/tests/test_xopt.py +++ b/xopt/tests/test_xopt.py @@ -599,6 +599,22 @@ def test_data_dump_file(self, tmp_path): dumped_data, X.data, check_dtype=False, check_names=False ) + def test_dump_file_expandvars(self, tmp_path, monkeypatch): + evaluator = Evaluator(function=xtest_callable) + generator = RandomGenerator(vocs=deepcopy(TEST_VOCS_BASE)) + + monkeypatch.setenv("XOPT_TEST_DUMP_DIR", str(tmp_path)) + X = Xopt( + generator=generator, + evaluator=evaluator, + xopt_dump_file="$XOPT_TEST_DUMP_DIR/dump.yml", + data_dump_file="$XOPT_TEST_DUMP_DIR/data.csv", + ) + X.random_evaluate(1) + + assert os.path.exists(tmp_path / "dump.yml") + assert os.path.exists(tmp_path / "data.csv") + def test_dump_without_files(self, tmp_path): evaluator = Evaluator(function=xtest_callable) generator = RandomGenerator(vocs=deepcopy(TEST_VOCS_BASE)) From acba3d26c723026dc0f12c8f16bb1764290bffa0 Mon Sep 17 00:00:00 2001 From: "Christopher M. Pierce" Date: Sat, 8 Aug 2026 01:53:26 -0700 Subject: [PATCH 5/8] add remaining test coverage --- xopt/tests/test_xopt.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/xopt/tests/test_xopt.py b/xopt/tests/test_xopt.py index 6d6c9a235..ad43549f4 100644 --- a/xopt/tests/test_xopt.py +++ b/xopt/tests/test_xopt.py @@ -569,6 +569,25 @@ def test_dump_file_legacy_name(self): X = Xopt(YAML) assert X.xopt_dump_file == "test_checkpointing.yaml" + def test_dump_file_legacy_property(self): + evaluator = Evaluator(function=xtest_callable) + generator = RandomGenerator(vocs=deepcopy(TEST_VOCS_BASE)) + + X = Xopt( + generator=generator, + evaluator=evaluator, + xopt_dump_file="test_checkpointing.yaml", + ) + + # Reading through the old name warns and gives the current value + with pytest.warns(DeprecationWarning, match="renamed to `xopt_dump_file`"): + assert X.dump_file == "test_checkpointing.yaml" + + # Assigning through the old name warns and writes through + with pytest.warns(DeprecationWarning, match="renamed to `xopt_dump_file`"): + X.dump_file = "other_checkpointing.yaml" + assert X.xopt_dump_file == "other_checkpointing.yaml" + def test_dump_file_legacy_conflict(self): evaluator = Evaluator(function=xtest_callable) generator = RandomGenerator(vocs=deepcopy(TEST_VOCS_BASE)) From 8d9a04e12f2a20f68985c5f0f7e93889067d4fab Mon Sep 17 00:00:00 2001 From: "Christopher M. Pierce" Date: Sat, 15 Aug 2026 01:33:34 -0700 Subject: [PATCH 6/8] only report attribute conflict when `dump_file` is set --- xopt/base.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/xopt/base.py b/xopt/base.py index 214d43c27..6a2125d85 100644 --- a/xopt/base.py +++ b/xopt/base.py @@ -264,12 +264,14 @@ def dump_file_legacy(cls, data: Any): """ if isinstance(data, dict) and "dump_file" in data: warnings.warn(DUMP_FILE_RENAME_MESSAGE, DeprecationWarning, stacklevel=2) - if data.get("xopt_dump_file") is not None: - raise ValueError( - "Cannot specify both 'dump_file' and 'xopt_dump_file'. " - "Use 'xopt_dump_file' instead." - ) - data["xopt_dump_file"] = data.pop("dump_file") + dump_file = data.pop("dump_file") + if dump_file is not None: + if data.get("xopt_dump_file") is not None: + raise ValueError( + "Cannot specify both 'dump_file' and 'xopt_dump_file'. " + "Use 'xopt_dump_file' instead." + ) + data["xopt_dump_file"] = dump_file return data @property From 62d5e45cfb8922dbd123b2d5536ae00572eb2e1d Mon Sep 17 00:00:00 2001 From: "Christopher M. Pierce" Date: Sat, 15 Aug 2026 01:48:31 -0700 Subject: [PATCH 7/8] break out data dump method --- xopt/base.py | 80 +++++++++++++++++++++++++++-------------- xopt/tests/test_xopt.py | 20 +++++++++-- 2 files changed, 70 insertions(+), 30 deletions(-) diff --git a/xopt/base.py b/xopt/base.py index 6a2125d85..e89e432de 100644 --- a/xopt/base.py +++ b/xopt/base.py @@ -105,8 +105,10 @@ class Xopt(XoptBaseModel): Xopt. yaml(**kwargs) Serializes the Xopt configuration to a YAML string. - dump(file: str = None, data_file: str = None, **kwargs) - Dumps the Xopt configuration and/or the evaluation data to the specified files. + dump(file: str = None, **kwargs) + Dumps the Xopt configuration to a specified file. + dump_data() + Dumps the evaluation data to the file given by `data_dump_file`. dict(**kwargs) -> dict Provides a custom dictionary representation of the Xopt configuration. json(**kwargs) -> str @@ -482,7 +484,10 @@ def evaluate_data( self.add_data(output_data) # dump to file(s) if specified - self.dump() + if self.xopt_dump_file is not None: + self.dump() + if self.data_dump_file is not None: + self.dump_data() return output_data @@ -662,41 +667,62 @@ def yaml(self, **kwargs): ) return yaml.dump(output) - def dump(self, file: str = None, data_file: str = None, **kwargs): + def dump(self, file: str = None, **kwargs): """ - Dump the Xopt object and/or the evaluation data to file. + Dump the Xopt configuration and data to a YAML file. - Each target is written only if a path is available for it, either via argument - or via the corresponding attribute. If neither is available nothing is written. - Environment variables in the paths are expanded here, so the unexpanded paths - are the ones stored on the object and written to the serialized configuration. + Environment variables in the path are expanded here, so the unexpanded path + is the one stored on the object and written to the serialized configuration. Parameters ---------- file : str, optional - The path to the YAML file where the Xopt configuration will be dumped. + The path to the file where the Xopt configuration will be dumped. Defaults to the `xopt_dump_file` attribute. - data_file : str, optional - The path to the CSV file where the evaluation data will be dumped. - Defaults to the `data_dump_file` attribute. **kwargs - Additional keyword arguments for customizing the YAML serialization. + Additional keyword arguments for customizing the dump. + + Raises + ------ + ValueError + If no dump file is specified via argument or in the `xopt_dump_file` + attribute. """ fname = file if file is not None else self.xopt_dump_file - data_fname = data_file if data_file is not None else self.data_dump_file - - if fname is not None: - fname = os.path.expandvars(fname) - with open(fname, "w") as f: - f.write(self.yaml(**kwargs)) - logger.debug(f"Dumped state to YAML file: {fname}") - - if data_fname is not None: - data_fname = os.path.expandvars(data_fname) - data = self.data if self.data is not None else pd.DataFrame() - data.to_csv(data_fname, index_label="xopt_index") - logger.debug(f"Dumped data to CSV file: {data_fname}") + + if fname is None: + raise ValueError( + "no dump file specified via argument or in `xopt_dump_file` attribute" + ) + + fname = os.path.expandvars(fname) + with open(fname, "w") as f: + f.write(self.yaml(**kwargs)) + logger.debug(f"Dumped state to YAML file: {fname}") + + def dump_data(self): + """ + Dump the evaluation data to the CSV file given by `data_dump_file`. + + Environment variables in the path are expanded here, so the unexpanded path + is the one stored on the object and written to the serialized configuration. + + Raises + ------ + ValueError + If no data dump file is specified in the `data_dump_file` attribute. + + """ + if self.data_dump_file is None: + raise ValueError( + "no data dump file specified in `data_dump_file` attribute" + ) + + fname = os.path.expandvars(self.data_dump_file) + data = self.data if self.data is not None else pd.DataFrame() + data.to_csv(fname, index_label="xopt_index") + logger.debug(f"Dumped data to CSV file: {fname}") def dict(self, **kwargs) -> dict: """ diff --git a/xopt/tests/test_xopt.py b/xopt/tests/test_xopt.py index ad43549f4..ae0b4d6d8 100644 --- a/xopt/tests/test_xopt.py +++ b/xopt/tests/test_xopt.py @@ -618,6 +618,14 @@ def test_data_dump_file(self, tmp_path): dumped_data, X.data, check_dtype=False, check_names=False ) + # dumping directly writes the same data + os.remove(data_dump_file) + X.dump_data() + dumped_data = pd.read_csv(data_dump_file, index_col="xopt_index") + pd.testing.assert_frame_equal( + dumped_data, X.data, check_dtype=False, check_names=False + ) + def test_dump_file_expandvars(self, tmp_path, monkeypatch): evaluator = Evaluator(function=xtest_callable) generator = RandomGenerator(vocs=deepcopy(TEST_VOCS_BASE)) @@ -639,12 +647,18 @@ def test_dump_without_files(self, tmp_path): generator = RandomGenerator(vocs=deepcopy(TEST_VOCS_BASE)) X = Xopt(generator=generator, evaluator=evaluator) - X.random_evaluate(1) - # no dump files specified, nothing should be written and no error raised - X.dump() + # no dump files specified, evaluation should not write anything or raise + X.random_evaluate(1) assert not list(tmp_path.iterdir()) + # dumping explicitly requires a destination + with pytest.raises(ValueError): + X.dump() + + with pytest.raises(ValueError): + X.dump_data() + def test_random_evaluate(self): evaluator = Evaluator(function=xtest_callable) generator = RandomGenerator(vocs=deepcopy(TEST_VOCS_BASE)) From 4bc8695336ec35a27373f8ffc696d9be1d2a5bb6 Mon Sep 17 00:00:00 2001 From: "Christopher M. Pierce" Date: Sat, 15 Aug 2026 01:51:19 -0700 Subject: [PATCH 8/8] expand user home char (~) --- xopt/base.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/xopt/base.py b/xopt/base.py index e89e432de..485070f5d 100644 --- a/xopt/base.py +++ b/xopt/base.py @@ -67,10 +67,11 @@ class Xopt(XoptBaseModel): optimization process. xopt_dump_file : str, optional An optional file path for dumping attributes of the xopt object and the - results of evaluations. Environment variables are expanded when dumping. + results of evaluations. Environment variables and `~` are expanded when + dumping. data_dump_file : str, optional An optional file path for dumping the evaluation data as a CSV file. - Environment variables are expanded when dumping. + Environment variables and `~` are expanded when dumping. data : DataFrame, optional An optional DataFrame object for storing internal data related to the optimization process. @@ -671,8 +672,7 @@ def dump(self, file: str = None, **kwargs): """ Dump the Xopt configuration and data to a YAML file. - Environment variables in the path are expanded here, so the unexpanded path - is the one stored on the object and written to the serialized configuration. + Environment variables and `~` in the path are expanded. Parameters ---------- @@ -696,7 +696,7 @@ def dump(self, file: str = None, **kwargs): "no dump file specified via argument or in `xopt_dump_file` attribute" ) - fname = os.path.expandvars(fname) + fname = os.path.expanduser(os.path.expandvars(fname)) with open(fname, "w") as f: f.write(self.yaml(**kwargs)) logger.debug(f"Dumped state to YAML file: {fname}") @@ -705,8 +705,7 @@ def dump_data(self): """ Dump the evaluation data to the CSV file given by `data_dump_file`. - Environment variables in the path are expanded here, so the unexpanded path - is the one stored on the object and written to the serialized configuration. + Environment variables and `~` in the path are expanded. Raises ------ @@ -719,7 +718,7 @@ def dump_data(self): "no data dump file specified in `data_dump_file` attribute" ) - fname = os.path.expandvars(self.data_dump_file) + fname = os.path.expanduser(os.path.expandvars(self.data_dump_file)) data = self.data if self.data is not None else pd.DataFrame() data.to_csv(fname, index_label="xopt_index") logger.debug(f"Dumped data to CSV file: {fname}")