diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 4e9273c2cd..1379485e74 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -1,10 +1,9 @@ name: Benchmarks on: - pull_request: + pull_request_target: branches: - master - pull_request_target: types: [opened, synchronize] workflow_dispatch: inputs: diff --git a/.github/workflows/changelog_update.yml b/.github/workflows/changelog_update.yml new file mode 100644 index 0000000000..c3b1691be6 --- /dev/null +++ b/.github/workflows/changelog_update.yml @@ -0,0 +1,35 @@ +name: Check changelog updated + +on: + pull_request: + branches: + - master + types: [opened, synchronize, labeled, unlabeled] + +jobs: + check_changelog_updated: + runs-on: ubuntu-latest + steps: + - name: Filter changes + id: changes + uses: dorny/paths-filter@v3 + with: + filters: | + has_changes: + - 'desc/**' + - 'requirements.txt' + - 'requirements_conda.yml' + - '.github/workflows/changelog_update.yml' + + - name: Check for relevant changes + id: check_changes + run: echo "has_changes=${{ steps.changes.outputs.has_changes }}" >> $GITHUB_ENV + + - uses: actions/checkout@v4 + + - if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip_changelog') && env.has_changes == 'true'}} + uses: danieljimeneznz/ensure-files-changed@v4.1.0 + with: + require-changes-to: | + CHANGELOG.md + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/desc/plotting.py b/desc/plotting.py index 3a6dcdd76a..def22fa14d 100644 --- a/desc/plotting.py +++ b/desc/plotting.py @@ -1354,6 +1354,8 @@ def plot_section( * ``phi``: float, int or array-like. Toroidal angles to plot. If an integer, plot that number equally spaced in [0,2pi/NFP). Default 1 for axisymmetry and 6 for non-axisymmetry + * ``fill`` : bool, Whether the contours are filled, i.e. whether to use + `contourf` or `contour`. Default to ``fill=True`` Returns ------- @@ -1442,7 +1444,7 @@ def plot_section( R = coords["R"].reshape((nt, nr, nz), order="F") Z = coords["Z"].reshape((nt, nr, nz), order="F") data = data.reshape((nt, nr, nz), order="F") - + op = "contour" + ("f" if kwargs.pop("fill", True) else "") contourf_kwargs = {} if log: data = np.abs(data) # ensure data is positive for log plot @@ -1474,7 +1476,9 @@ def plot_section( for i in range(nphi): divider = make_axes_locatable(ax[i]) - cntr = ax[i].contourf(R[:, :, i], Z[:, :, i], data[:, :, i], **contourf_kwargs) + cntr = getattr(ax[i], op)( + R[:, :, i], Z[:, :, i], data[:, :, i], **contourf_kwargs + ) cax = divider.append_axes("right", **cax_kwargs) cbar = fig.colorbar(cntr, cax=cax) cbar.update_ticks() diff --git a/tests/baseline/test_section_chi_contour.png b/tests/baseline/test_section_chi_contour.png new file mode 100644 index 0000000000..ac8c342c18 Binary files /dev/null and b/tests/baseline/test_section_chi_contour.png differ diff --git a/tests/conftest.py b/tests/conftest.py index ccab0e07a6..54e60554d6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -284,13 +284,13 @@ def DummyMixedCoilSet(tmpdir_factory): vf_coil, displacement=[0, 0, 2], n=3, endpoint=True ) xyz_coil = FourierXYZCoil(current=2) - phi = 2 * np.pi * np.linspace(0, 1, 20, endpoint=True) ** 2 + phi = 2 * np.pi * np.linspace(0, 1, 20, endpoint=True) spline_coil = SplineXYZCoil( current=1, X=np.cos(phi), Y=np.sin(phi), Z=np.zeros_like(phi), - knots=np.linspace(0, 2 * np.pi, len(phi)), + knots=phi, ) full_coilset = MixedCoilSet( (tf_coilset, vf_coilset, xyz_coil, spline_coil), check_intersection=False diff --git a/tests/test_examples.py b/tests/test_examples.py index 5b4e328981..839c685def 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1324,7 +1324,20 @@ def test_second_stage_optimization_CoilSet(): @pytest.mark.slow @pytest.mark.unit -def test_optimize_with_all_coil_types(DummyCoilSet, DummyMixedCoilSet): +@pytest.mark.parametrize( + "coil_type", + [ + "FourierPlanarCoil", + "FourierRZCoil", + "FourierXYZCoil", + "SplineXYZCoil", + "CoilSet sym", + "CoilSet asym", + "MixedCoilSet", + "nested CoilSet", + ], +) +def test_optimize_with_all_coil_types(DummyCoilSet, DummyMixedCoilSet, coil_type): """Test optimizing for every type of coil and dummy coil sets.""" sym_coils = load(load_from=str(DummyCoilSet["output_path_sym"]), file_format="hdf5") asym_coils = load( @@ -1340,66 +1353,63 @@ def test_optimize_with_all_coil_types(DummyCoilSet, DummyMixedCoilSet): quad_eval_grid = LinearGrid(M=2, sym=True) quad_field_grid = LinearGrid(N=2) - def test(c, method): - target = 11 - rtol = 1e-3 - # first just check that quad flux works for a couple iterations - # as this is an expensive objective to compute - obj = ObjectiveFunction( - QuadraticFlux( - eq=eq, - field=c, - vacuum=True, - weight=1e-4, - eval_grid=quad_eval_grid, - field_grid=quad_field_grid, - ) - ) - optimizer = Optimizer(method) - (c,), _ = optimizer.optimize(c, obj, maxiter=2, ftol=0, xtol=1e-15) - - # now check with optimizing geometry and actually check result - objs = [ - CoilLength(c, target=target), - ] - extra_msg = "" - if isinstance(c, MixedCoilSet): - # just to check they work without error - objs.extend( - [ - CoilCurvature(c, target=0.5, weight=1e-2), - CoilTorsion(c, target=0, weight=1e-2), - ] - ) - rtol = 3e-2 - extra_msg = " with curvature and torsion obj" - - obj = ObjectiveFunction(objs) - - (c,), _ = optimizer.optimize(c, obj, maxiter=25, ftol=5e-3, xtol=1e-15) - flattened_coils = tree_leaves( - c, is_leaf=lambda x: isinstance(x, _Coil) and not isinstance(x, CoilSet) - ) - lengths = [coil.compute("length")["length"] for coil in flattened_coils] - np.testing.assert_allclose( - lengths, target, rtol=rtol, err_msg=f"lengths {c}" + extra_msg - ) - spline_coil = mixed_coils.coils[-1].copy() - # single coil - test(FourierPlanarCoil(), "fmintr") - test(FourierRZCoil(), "fmintr") - test(FourierXYZCoil(), "fmintr") - test(spline_coil, "fmintr") + types = { + "FourierPlanarCoil": (FourierPlanarCoil(), "fmintr"), + "FourierRZCoil": (FourierRZCoil(), "fmintr"), + "FourierXYZCoil": (FourierXYZCoil(), "fmintr"), + "SplineXYZCoil": (spline_coil, "fmintr"), + "CoilSet sym": (sym_coils, "lsq-exact"), + "CoilSet asym": (asym_coils, "lsq-exact"), + "MixedCoilSet": (mixed_coils, "lsq-exact"), + "nested CoilSet": (nested_coils, "lsq-exact"), + } + c, method = types[coil_type] + + target = 11 + rtol = 1e-3 + # first just check that quad flux works for a couple iterations + # as this is an expensive objective to compute + obj = ObjectiveFunction( + QuadraticFlux( + eq=eq, + field=c, + vacuum=True, + weight=1e-4, + eval_grid=quad_eval_grid, + field_grid=quad_field_grid, + ) + ) + optimizer = Optimizer(method) + (cc,), _ = optimizer.optimize(c, obj, maxiter=2, ftol=0, xtol=1e-8, copy=True) + + # now check with optimizing geometry and actually check result + objs = [ + CoilLength(c, target=target), + ] + extra_msg = "" + if isinstance(c, MixedCoilSet): + # just to check they work without error + objs.extend( + [ + CoilCurvature(c, target=0.5, weight=1e-2), + CoilTorsion(c, target=0, weight=1e-2), + ] + ) + rtol = 3e-2 + extra_msg = " with curvature and torsion obj" - # CoilSet - test(sym_coils, "lsq-exact") - test(asym_coils, "lsq-exact") + obj = ObjectiveFunction(objs) - # MixedCoilSet - test(mixed_coils, "lsq-exact") - test(nested_coils, "lsq-exact") + (c,), _ = optimizer.optimize(c, obj, maxiter=25, ftol=5e-3, xtol=1e-8) + flattened_coils = tree_leaves( + c, is_leaf=lambda x: isinstance(x, _Coil) and not isinstance(x, CoilSet) + ) + lengths = [coil.compute("length")["length"] for coil in flattened_coils] + np.testing.assert_allclose( + lengths, target, rtol=rtol, err_msg=f"lengths {c}" + extra_msg + ) @pytest.mark.unit diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 8488c91a87..44a3c5c2da 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -429,6 +429,14 @@ def test_section_J(self): return fig + @pytest.mark.unit + @pytest.mark.mpl_image_compare(remove_text=True, tolerance=tol_2d) + def test_section_chi_contour(self): + """Test plotting poincare section of poloidal flux, with fill=False.""" + eq = get("DSHAPE_CURRENT") + fig, ax = plot_section(eq, "chi", fill=False, levels=20) + return fig + @pytest.mark.unit @pytest.mark.mpl_image_compare(remove_text=True, tolerance=tol_2d) def test_section_F(self):