Skip to content

Commit 9b2eca4

Browse files
authored
Merge branch 'main' into share-profile-geometry
2 parents d5da669 + 82ef798 commit 9b2eca4

35 files changed

Lines changed: 4326 additions & 384 deletions

.github/workflows/notebooks.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ jobs:
3030
run: jupyter nbconvert --execute --to notebook --inplace "examples/example03 - extracting_equilibrium_quantities.ipynb"
3131
- name: Check using magnetic probes notebook
3232
run: jupyter nbconvert --execute --to notebook --inplace "examples/example04 - using_magnetic_probes.ipynb"
33-
- name: Check evolutive forward solve notebook
33+
- name: Check evolutive forward solve notebook (5a)
3434
run: jupyter nbconvert --execute --to notebook --inplace "examples/example05a - nonlinear_and_linear_evolution_with_GS.ipynb"
35-
- name: Check evolutive forward solve notebook
35+
- name: Check evolutive forward solve notebook (5b)
3636
run: jupyter nbconvert --execute --to notebook --inplace "examples/example05b - linear_evolution_without_GS.ipynb"
37-
- name: Check evolutive forward solve notebook
37+
- name: Check evolutive forward solve notebook (5c)
3838
run: jupyter nbconvert --execute --to notebook --inplace "examples/example05c - linear_evolution_with_relinearisation.ipynb"
3939
- name: Check static inverse solve Anamak notebook
4040
run: jupyter nbconvert --execute --to notebook --inplace "examples/example07a - Anamak"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ These problems can be solved in a **user-specified tokamak geometry** that can i
2929
| ------ | ------ | ------ | ------ |
3030
| Active poloidal field coils | Can be assigned (voltage-driven) currents that influence plasma shape and position. | Locations, sizes (areas), wirings (series/anti-series), polarities (+1 or -1), resistivities (of coil materials), and number of windings. | Blue rectangles |
3131
| Passive conducting structures | Can be assigned induced eddy currents that also impact plasma shape and position. In evolutive forward mode, these are solved self-consistently. | Locations, sizes, orientations (if available), and filaments (as passives can be refined if needed). | Dark grey parallelograms |
32-
| Wall and/or limiter contours | Confines the plasma boundary (for computational purposes). | Locations. | Solid black line |
32+
| Wall and/or limiter contours | Confines the plasma boundary (for computational purposes). | Locations; the limiter contour must lie strictly inside the equilibrium solution domain. | Solid black line |
3333
| Magnetic diagnostic probes | Can measure the poloidal flux (fluxloops) or the magnetic field strength (pickup coils) at specified locations. | Locations (for both) and orientations (for pickup coils). | Orange diamonds (fluxloops) and brown dots/lines (pickup coils) |
3434

3535
Static Grad-Shafranov problems are solved using **fourth-order accurate finite differences** and a **purpose-built Newton-Krylov method** for additional **stability and convergence** speed (over the Picard iterations used in FreeGS). An implicit Euler method and the same Newton-Krylov solver are used to tackle the evolutive problem.

examples/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ These example Jupyter notebooks are intended to be the **first port of call for
2121
| Example 10 | Learn how to calculate growth rates associated with vertically unstable modes. | Anyone |
2222
| Example 11 | Learn how to use the evolutive solver alongside a virtual plasma control system (FreeGSNKE Pulse Design Tool). | Anyone |
2323

24-
If a new example has been created, please add a new line to the table explaining its purpose!
24+
If a new example has been created, please add a new line to the table explaining its purpose!

examples/example00 - build_tokamak_machine.ipynb

Lines changed: 157 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@
375375
"\n",
376376
"For diverted plasmas, the limiter has no effect on the equilibrium or its evolution. For limiter plasmas, the last closed flux surface will be tangent to the limiter. FreeGSNKE will also force the last closed flux surface to lie entirely inside the region allowed by the limiter. In other words, outside the limiter, the constraint $J=0$ on the plasma current density is enforced. \n",
377377
"\n",
378-
"The format of the limiter coordinates for FreeGSNKE is a simple list of dictionaries with `R` and `Z` coordinates."
378+
"The format of the limiter coordinates for FreeGSNKE is a simple list of dictionaries with `R` and `Z` coordinates. When an `Equilibrium` is later instantiated, the whole limiter contour must lie strictly inside the rectangular solution domain defined by `Rmin`, `Rmax`, `Zmin`, and `Zmax`. Active coils and passive structures may sit outside this grid, but the limiter itself may not."
379379
]
380380
},
381381
{
@@ -699,6 +699,161 @@
699699
"ax1.set_xlabel(r'Major radius, $R$ [m]')\n",
700700
"ax1.set_ylabel(r'Height, $Z$ [m]')"
701701
]
702+
},
703+
{
704+
"cell_type": "markdown",
705+
"metadata": {},
706+
"source": [
707+
"## Updating an existing machine description\n",
708+
"\n",
709+
"If you already have a `tokamak` object, you can update its machine description directly from dictionaries or pickle paths.\n",
710+
"\n",
711+
"This is useful when you want to test small geometry changes without writing new pickle files or rebuilding every object from scratch.\n",
712+
"\n",
713+
"If the `tokamak` is already attached to an `Equilibrium` object (see the next example notebook), use `eq.update_machine_description(...)` instead so the equilibrium refreshes its cached Green's functions and limiter masks automatically. Note that after changing the machine geometry, any existing solver objects should be reinstantiated (again, see next notebooks for solver objects!).\n",
714+
"\n",
715+
"Let's start by moving the `Pz` coil radially outward!"
716+
]
717+
},
718+
{
719+
"cell_type": "code",
720+
"execution_count": null,
721+
"metadata": {},
722+
"outputs": [],
723+
"source": [
724+
"from copy import deepcopy\n",
725+
"\n",
726+
"# preserve an existing current so we can see it survive the machine update\n",
727+
"tokamak_alt.set_coil_current(\"Pz\", 1.0e3)\n",
728+
"\n",
729+
"# extract the old R coords for Pz\n",
730+
"old_pz_R = tokamak_alt.coils_dict[\"Pz\"][\"coords\"][0].copy()\n",
731+
"\n",
732+
"# work on a copy of the machine-description data already built above.\n",
733+
"updated_active_coils = deepcopy(active_coils)\n",
734+
"\n",
735+
"# modify the Pz coords in this dictionary\n",
736+
"updated_active_coils[\"Pz\"][\"\"][\"R\"] = [\n",
737+
" r + 0.6 for r in updated_active_coils[\"Pz\"][\"\"][\"R\"]\n",
738+
"]\n",
739+
"\n",
740+
"# call this function (faster than re-building the whole machine as it only looks at changed coils)\n",
741+
"tokamak_alt.set_machine_description(\n",
742+
" active_coils_data=updated_active_coils,\n",
743+
" passive_coils_data=passive_coils,\n",
744+
" limiter_data=limiter,\n",
745+
" wall_data=wall,\n",
746+
" magnetic_probe_data=magnetic_probes,\n",
747+
")"
748+
]
749+
},
750+
{
751+
"cell_type": "code",
752+
"execution_count": null,
753+
"metadata": {},
754+
"outputs": [],
755+
"source": [
756+
"# check that the coil has been moved\n",
757+
"new_pz_R = tokamak_alt.coils_dict[\"Pz\"][\"coords\"][0]\n",
758+
"print(f\"Pz moved from R = {old_pz_R} to R = {new_pz_R}\")\n",
759+
"\n",
760+
"# check the coil current didn't change\n",
761+
"print(f\"Preserved Pz current: {tokamak_alt['Pz'].current} A\")"
762+
]
763+
},
764+
{
765+
"cell_type": "code",
766+
"execution_count": null,
767+
"metadata": {},
768+
"outputs": [],
769+
"source": [
770+
"# plot the updated machine and notice Pz outside the vessel\n",
771+
"fig1, ax1 = plt.subplots(1, 1, figsize=(4, 8), dpi=80)\n",
772+
"plt.tight_layout()\n",
773+
"\n",
774+
"tokamak_alt.plot(axis=ax1, show=False)\n",
775+
"ax1.plot(tokamak_alt.limiter.R, tokamak_alt.limiter.Z, color=\"k\", linewidth=1.2, linestyle=\"--\")\n",
776+
"ax1.plot(tokamak_alt.wall.R, tokamak_alt.wall.Z, color=\"k\", linewidth=1.2, linestyle=\"-\")\n",
777+
"\n",
778+
"ax1.grid(alpha=0.5)\n",
779+
"ax1.set_aspect(\"equal\")\n",
780+
"ax1.set_xlabel(r\"Major radius, $R$ [m]\")\n",
781+
"ax1.set_ylabel(r\"Height, $Z$ [m]\")\n"
782+
]
783+
},
784+
{
785+
"cell_type": "markdown",
786+
"metadata": {},
787+
"source": [
788+
"## Updating an active coil dictionary individually\n",
789+
"\n",
790+
"For optimization workflows that perturb one active coil (or circuit) at a time, we can use `tokamak.update_active_coil(coil_name, active_coil_data)`. \n",
791+
"\n",
792+
"This takes the data stored under that coil's label in the active-coils dictionary, updates only that active coil/circuit, and recalculates only the affected R/M matrix entries. On an equilibrium object, use `eq.update_active_coil(...)` so only the affected coil Greens function is refreshed (see next notebooks!).\n",
793+
"\n",
794+
"For example, you may want to do this in **machine geometry optimisation** studies, e.g. [Nunn et al. (2025)](https://pubs.aip.org/aip/pop/article/32/7/072507/3355562/Bayesian-optimization-of-poloidal-field-coil).\n",
795+
"\n",
796+
"Let's move the `P1` circuit vertically outward this time."
797+
]
798+
},
799+
{
800+
"cell_type": "code",
801+
"execution_count": null,
802+
"metadata": {},
803+
"outputs": [],
804+
"source": [
805+
"# old P1 coil data\n",
806+
"old_p1_Z = tokamak_alt.coils_dict[\"P1\"][\"coords\"][1].copy()\n",
807+
"\n",
808+
"# extract only the P1 circuit data\n",
809+
"updated_p1 = deepcopy(updated_active_coils[\"P1\"])\n",
810+
"\n",
811+
"# make changes\n",
812+
"updated_p1[\"upper\"][\"Z\"] = [z + 0.4 for z in updated_p1[\"upper\"][\"Z\"]]\n",
813+
"updated_p1[\"lower\"][\"Z\"] = [z - 0.4 for z in updated_p1[\"lower\"][\"Z\"]]\n",
814+
"\n",
815+
"# update the circuit in the tokamak\n",
816+
"tokamak_alt.update_active_coil(\"P1\", updated_p1)\n",
817+
"\n",
818+
"# show the changes\n",
819+
"new_p1_Z = tokamak_alt.coils_dict[\"P1\"][\"coords\"][1]\n",
820+
"print(f\"P1 moved from Z = {old_p1_Z} to Z = {new_p1_Z}\")\n",
821+
"print(f\"Changed coils recorded: {tokamak_alt._last_machine_update_changed_coils}\")"
822+
]
823+
},
824+
{
825+
"cell_type": "code",
826+
"execution_count": null,
827+
"metadata": {},
828+
"outputs": [],
829+
"source": [
830+
"# plot the updated machine\n",
831+
"fig1, ax1 = plt.subplots(1, 1, figsize=(4, 8), dpi=80)\n",
832+
"plt.tight_layout()\n",
833+
"\n",
834+
"tokamak_alt.plot(axis=ax1, show=False)\n",
835+
"ax1.plot(tokamak_alt.limiter.R, tokamak_alt.limiter.Z, color=\"k\", linewidth=1.2, linestyle=\"--\")\n",
836+
"ax1.plot(tokamak_alt.wall.R, tokamak_alt.wall.Z, color=\"k\", linewidth=1.2, linestyle=\"-\")\n",
837+
"\n",
838+
"ax1.grid(alpha=0.5)\n",
839+
"ax1.set_aspect(\"equal\")\n",
840+
"ax1.set_xlabel(r\"Major radius, $R$ [m]\")\n",
841+
"ax1.set_ylabel(r\"Height, $Z$ [m]\")\n"
842+
]
843+
},
844+
{
845+
"cell_type": "code",
846+
"execution_count": null,
847+
"metadata": {},
848+
"outputs": [],
849+
"source": []
850+
},
851+
{
852+
"cell_type": "code",
853+
"execution_count": null,
854+
"metadata": {},
855+
"outputs": [],
856+
"source": []
702857
}
703858
],
704859
"metadata": {
@@ -717,7 +872,7 @@
717872
"name": "python",
718873
"nbconvert_exporter": "python",
719874
"pygments_lexer": "ipython3",
720-
"version": "3.10.16"
875+
"version": "3.10.20"
721876
}
722877
},
723878
"nbformat": 4,

examples/example01a - static_inverse_solve_MASTU.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@
116116
"\n",
117117
"We are now ready to build a plasma equilibrium object for our tokamak. This is done using the `freegs4e.Equilibrium` class, which implicitly defines the rectangular domain of the solver as well as the grid resolution.\n",
118118
"\n",
119-
"`Equilibrium` has sensible defaults, but it is recommended to define the radial and vertical domain of the grid using the `Rmin`, `Rmax`, `Zmin` and `Zmax` parameters, as well as the grid resolution in the radial and vertical directions with the `nx` and `ny` parameters. The grid will be initialised using fourth-order finite differences. Note that the computational grid **must** encompass the domain enclosed by the limiter object (as this is where the plasma will be confined to). Though it does not need to encompass any/all of the active coils or passive structures. \n",
119+
"`Equilibrium` has sensible defaults, but it is recommended to define the radial and vertical domain of the grid using the `Rmin`, `Rmax`, `Zmin` and `Zmax` parameters, as well as the grid resolution in the radial and vertical directions with the `nx` and `ny` parameters. The grid will be initialised using fourth-order finite differences. Note that the limiter contour **must** lie strictly inside the computational grid, because limiter-boundary flux interpolation is used when identifying limited plasmas. The grid does not need to encompass any/all of the active coils or passive structures. \n",
120120
"\n",
121121
"A tokamak object should be supplied to the `tokamak` parameter to assign the desired machine to the equilibrium.\n",
122122
"\n",

0 commit comments

Comments
 (0)