diff --git a/CHANGELOG.md b/CHANGELOG.md index f609e6ad8c..1b1f07a247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,17 @@ v0.17.1 Bug Fixes +- Fixes the possible permission error in `from_input_file` method of `Equilibrium`, `FourierRZToroidalSurface` and `FourierRZCurve` classes when used with a VMEC input file. Now the automatically generated DESC input file is written to a temporary file under `./tmp/`. + +v0.17.1 +------- + +This patch release updates package dependencies to resolve critical incompatibilities between jax-related libraries. DESC versions 0.16.0 and 0.17.0 contain a known bug when used alongside jax-finufft v1.3.0, and we recommend that users upgrade to DESC version 0.17.1 to ensure environment stability. + +If an immediate upgrade is not feasible, the issue can be mitigated by manually downgrading jax-finufft to version 1.2.0. + +Bug Fixes + - Fixes incorrect units in the documentation of some curvature variables. diff --git a/desc/input_reader.py b/desc/input_reader.py index 09f06b9eaf..9859e1d391 100644 --- a/desc/input_reader.py +++ b/desc/input_reader.py @@ -215,10 +215,13 @@ def parse_inputs(self, fname=None): # noqa: C901 isVMEC = re.search(r"&INDATA", line, re.IGNORECASE) if isVMEC: print("Converting VMEC input to DESC input") - path = self.input_path + "_desc" - InputReader.vmec_to_desc_input(self.input_path, path) - print("Generated DESC input file {}:".format(path)) - return self.parse_inputs(path) + # use a buffer here to avoid having to read/write a file + # unnecessarily + buffer = io.StringIO() + InputReader.vmec_to_desc_input(self.input_path, buffer) + buffer.seek(0) + desc_file = buffer + return self.parse_inputs(desc_file) # extract numbers & words match = re.search(r"[!#]", line) @@ -680,9 +683,11 @@ def write_desc_input(filename, inputs, header=""): # noqa: C901 - fxn too compl """ # open the file, unless its already open + opened_here = False if not isinstance(filename, io.IOBase): filename = os.path.expanduser(filename) f = open(filename, "w+") + opened_here = True else: f = filename f.seek(0) @@ -783,7 +788,11 @@ def write_desc_input(filename, inputs, header=""): # noqa: C901 - fxn too compl for n, R0, Z0 in inputs[0]["axis"]: f.write("n: {:3d}\tR0 = {:16.8E}\tZ0 = {:16.8E}\n".format(int(n), R0, Z0)) - f.close() + # only close the file if we opened it here, as we also + # can use this function to write to an already open file + # or to an in-memory buffer + if opened_here: + f.close() @staticmethod def desc_output_to_input( # noqa: C901 - fxn too complex diff --git a/tests/test_input_output.py b/tests/test_input_output.py index ab4f362994..66a4602e21 100644 --- a/tests/test_input_output.py +++ b/tests/test_input_output.py @@ -45,8 +45,6 @@ def test_vmec_input(tmpdir_factory): with pytest.warns(UserWarning): ir = InputReader(cl_args=[str(tmp_path)]) vmec_inputs = ir.inputs - # ir makes a VMEC file automatically - path_converted_file = tmpdir.join("input.DSHAPE_desc") # also test making a DESC file from the ir.inputs manually path = tmpdir.join("desc_from_vmec") ir.write_desc_input(path, ir.inputs) @@ -60,6 +58,13 @@ def test_vmec_input(tmpdir_factory): correct_file_path = ".//tests//inputs//input.DSHAPE_desc" # check DESC input file matches known correct one line-by-line + # above InputReader(cl_args=[str(tmp_path)]) uses a temp buffer + # instead of the old behavior of writing an intermediate file (#2148) + # so we re-create the input file here at a specific + # location, so that we can compare + path_converted_file = tmpdir.join("input.DSHAPE_converted") + with pytest.warns(UserWarning): + InputReader.vmec_to_desc_input(input_path, str(path_converted_file)) with open(correct_file_path) as f: lines_correct = f.readlines() with open(path) as f: @@ -238,20 +243,30 @@ def test_near_axis_input_files(): np.testing.assert_allclose( inputs_desc[arg], inputs_vmec[arg], rtol=1e-6, atol=1e-8 ) - if os.path.exists(".//tests//inputs//input.QSC_r2_5.5_vmec_desc"): - os.remove(".//tests//inputs//input.QSC_r2_5.5_vmec_desc") @pytest.mark.unit -def test_from_input_file_equilibrium_desc_vmec_DSHAPE(): - """Test that from_input_file works for DESC input files.""" - vmec_path = ".//tests//inputs//input.DSHAPE" +def test_from_input_file_equilibrium_desc_vmec_DSHAPE(tmp_path): + """Test that from_input_file works for DESC and VMEC input files.""" desc_path = ".//tests//inputs//input.DSHAPE_desc" kwargs = {"spectral_indexing": "fringe"} with pytest.warns(UserWarning, match="Left handed"): eq = Equilibrium.from_input_file(desc_path, **kwargs) - with pytest.warns(UserWarning): - eq_VMEC = Equilibrium.from_input_file(vmec_path, **kwargs) + + # load VMEC input from a read-only directory to ensure temp buffer works + # Related to issue #2139 + vmec_src = ".//tests//inputs//input.DSHAPE" + locked_dir = tmp_path / "locked" + locked_dir.mkdir() + shutil.copy(vmec_src, locked_dir / "input.DSHAPE") + os.chmod(locked_dir, 0o555) + try: + with pytest.warns(UserWarning): + eq_VMEC = Equilibrium.from_input_file( + str(locked_dir / "input.DSHAPE"), **kwargs + ) + finally: + os.chmod(locked_dir, 0o755) # make sure the loaded eqs are equivalent np.testing.assert_allclose(eq.R_lmn, eq_VMEC.R_lmn) @@ -304,9 +319,6 @@ def test_from_input_file_equilibrium_desc_vmec(): assert eq_VMEC.iota is None assert eq.sym == eq_VMEC.sym - if os.path.exists(".//tests//inputs//input.QSC_r2_5.5_vmec_desc"): - os.remove(".//tests//inputs//input.QSC_r2_5.5_vmec_desc") - @pytest.mark.unit def test_vmec_input_surface_threshold(): @@ -814,7 +826,7 @@ def test_io_OmnigenousField(tmpdir_factory): @pytest.mark.unit def test_io_file_like_object(tmpdir_factory): - """Test loading an equilibrium from a file-like object (BytesIO)""" + """Test loading an equilibrium from a file-like object (BytesIO).""" file_path = "./tests/inputs/iotest_HELIOTRON.h5" with open(file_path, "rb") as f: file_like_obj = BytesIO(f.read())