diff --git a/README.md b/README.md index a7e728a..d89c11a 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ FreeGS: Free boundary Grad-Shafranov solver [![py3comp](https://img.shields.io/badge/py3-compatible-brightgreen.svg)](https://img.shields.io/badge/py3-compatible-brightgreen.svg) [![Build Status](https://github.com/freegs-plasma/freegs/workflows/Tests/badge.svg)](https://github.com/freegs-plasma/freegs/workflows/Tests/badge.svg) [![codecov](https://codecov.io/gh/freegs-plasma/freegs/branch/master/graph/badge.svg?token=4dc6aHbu7K)](https://codecov.io/gh/freegs-plasma/freegs) +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/freegs-plasma/freegs/tutorial?labpath=tutorial.ipynb) This Python module calculates plasma equilibria for tokamak fusion experiments, by solving the Grad-Shafranov equation with free boundaries. Given a set of coils, @@ -50,6 +51,11 @@ Examples The Jupyter notebooks contain examples wuth additional notes + +* [tutorial.ipynb](./tutorial.ipynb) + +Launch tutorial in [![MyBinder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/freegs-plasma/freegs/tutorial?labpath=tutorial.ipynb) + * MAST-example.ipynb There are also some Python scripts to run short tests diff --git a/freegs/__init__.py b/freegs/__init__.py index 00f4846..914a8f9 100644 --- a/freegs/__init__.py +++ b/freegs/__init__.py @@ -28,6 +28,7 @@ along with FreeGS. If not, see . """ + from importlib_metadata import metadata from .equilibrium import Equilibrium diff --git a/tutorial.ipynb b/tutorial.ipynb new file mode 100644 index 0000000..5a874b3 --- /dev/null +++ b/tutorial.ipynb @@ -0,0 +1,620 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "FreeGS tutorial\n", + "============\n", + "\n", + "Getting started with toroidal equilibria and solving the Grad-Shafranov equation\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Toroidal currents\n", + "\n", + "The magnetic field due to a toroidal wire is calculated using Green's functions\n", + "https://github.com/freegs-plasma/freegs/blob/master/freegs/gradshafranov.py#L272\n", + "\n", + "These are written in terms of elliptic functions of the first and second kind\n", + "\n", + "**Note** Some care is needed because there are different definitions (e.g. if arg is squared)\n", + "https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.ellipk.html\n", + "https://mathworld.wolfram.com/EllipticIntegraloftheFirstKind.html\n", + "\n", + "First kind:\n", + "$K(m) = \\int_0^{\\pi/2}\\frac{1}{\\sqrt{1 - m \\sin^2\\left(t\\right)}}dt$\n", + "\n", + "Second kind:\n", + "$E(m) = \\int_0^{\\pi/2}\\sqrt{1 - m \\sin^2\\left(t\\right)}dt$\n", + "\n", + "Poloidal flux at $(R, Z)$ due to unit current at $(R_c, Z_c)$ is the current in the coil $I_c$ times the Green's function:\n", + "\n", + "$\\psi\\left(R, Z; R_c, Z_c\\right) = I_c G\\left(R, Z; R_c, Z_c\\right)$\n", + "\n", + "with Green's function given by:\n", + "$G\\left(R, Z; R_c, Z_c\\right) = \\frac{\\mu_0}{2\\pi} \\sqrt{RR_c}\\left[(2 - k^2)K\\left(k^2\\right) - 2E\\left(k^2\\right)\\right] / k$\n", + "where\n", + "$k^2 = 4RR_c / \\left[\\left(R + R_c\\right)^2 + \\left(Z - Z_c\\right)^2\\right]$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from freegs.coil import Coil\n", + "\n", + "coil_1 = Coil(1.0, -1.0, current = 1e3) # R = 1m, Z = -1m, current = 1kA\n", + "\n", + "# Make a 2D grid of R, Z values\n", + "# Note: Number of cells 65 = 2^n + 1 is useful later\n", + "R, Z = np.meshgrid(np.linspace(0.1, 1.5, 65), np.linspace(-1.5, 1.5, 65), indexing='ij')\n", + "\n", + "# Calculate poloidal flux psi due to coil:\n", + "psi = coil_1.psi(R, Z)\n", + "\n", + "plt.contour(R, Z, psi, 40)\n", + "plt.xlabel(\"Major radius R [m]\")\n", + "plt.ylabel(\"Height Z [m]\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It can be (initially) easier to visualise what the magnetic field looks like rather than the flux" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# Radial component of the magnetic field\n", + "B_R = coil_1.Br(R, Z)\n", + "# Vertical component\n", + "B_Z = coil_1.Bz(R, Z)\n", + "\n", + "# Poloidal field magnitude\n", + "B_p = np.sqrt(B_R**2 + B_Z**2)\n", + "\n", + "plt.contourf(R, Z, np.log(B_p), 50)\n", + "plt.streamplot(R.T, Z.T, B_R.T, B_Z.T)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Vertical magnetic field\n", + "\n", + "Combining two coils with current in the same direction produces a vertical magnetic field\n", + "(A Helmholtz coil)\n", + "\n", + "This is used for **radial** plasma position control" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "coil_1 = Coil(1.0, -1.0, current = 1e3) # R = 1m, Z = -1m, current = 1kA\n", + "coil_2 = Coil(1.0, 1.0, current = 1e3) # R = 1m, Z = +1m, current = 1kA\n", + "\n", + "B_R = coil_1.Br(R, Z) + coil_2.Br(R, Z)\n", + "B_Z = coil_1.Bz(R, Z) + coil_2.Bz(R, Z)\n", + "B_Z = coil_1.Bz(R, Z) + coil_2.Bz(R, Z)\n", + "plt.streamplot(R.T, Z.T, B_R.T, B_Z.T)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Radial magnetic field\n", + "\n", + "Coils with opposite currents produces a radial magnetic field.\n", + "\n", + "This is used for **vertical** plasma position control" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "coil_1 = Coil(1.0, -1.0, current = 1e3) # R = 1m, Z = -1m, current = 1kA\n", + "coil_2 = Coil(1.0, 1.0, current = -1e3) # R = 1m, Z = +1m, current = -1kA\n", + "\n", + "B_R = coil_1.Br(R, Z) + coil_2.Br(R, Z)\n", + "B_Z = coil_1.Bz(R, Z) + coil_2.Bz(R, Z)\n", + "B_Z = coil_1.Bz(R, Z) + coil_2.Bz(R, Z)\n", + "plt.streamplot(R.T, Z.T, B_R.T, B_Z.T)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Circuits\n", + "\n", + "Most tokamaks do not have a separate power supply for each coil. Instead they are often wired in pairs, either in series or anti-series.\n", + "\n", + "FreeGS defines a `Circuit` class to group coils together" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from freegs.machine import Circuit\n", + "\n", + "circuit_1 = Circuit([(\"P1L\", Coil(1.0, -1.0), 1.0),\n", + " (\"P1U\", Coil(1.0, 1.0), -1.0)], # Negative so anti-series\n", + " current = 1e3) # 1kA in this circuit\n", + "B_R = circuit_1.Br(R, Z)\n", + "B_Z = circuit_1.Bz(R, Z)\n", + "plt.streamplot(R.T, Z.T, B_R.T, B_Z.T)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Change of topic: Plasma current\n", + "\n", + "The plasma carries a current in the toroidal and poloidal direction. The toroidal plasma current generates a poloidal magnetic field that adds to the field from the poloidal field (PF) coils.\n", + "\n", + "The principle is the same as for the coils, except we're integrating a current density over the plasma: The poloidal flux at $(R, Z)$ is \n", + "\n", + "$\\psi_{plasma}\\left(R, Z\\right) = \\int_{R,Z}J_\\phi\\left(R', Z'\\right) G\\left(R, Z; R', Z'\\right) dR'dZ'$\n", + "\n", + "where $J_\\phi\\left(R, Z\\right)$ is the toroidal current density (in A/m$^2$) in the plasma.\n", + "\n", + "## Computationally efficient methods\n", + "\n", + "Unfortunately the brute-force way to calculate $\\psi_{plasma}\\left(R, Z\\right)$ using the above integral is very slow. For an $N\\times N$ mesh the time to calculate a $\\psi$ at every point goes like $N^4$.\n", + "(Note that techniques like fast multipole can improve this, but would be quite complex to implement).\n", + "\n", + "Instead we start from the differential form and solve a Laplacian-like equation for $\\psi$\n", + "\n", + "$\\Delta^*\\psi = R^2 \\nabla\\cdot\\frac{1}{R^2}\\nabla\\psi = -\\mu_0 R J_\\phi$\n", + "\n", + "Multi-grid solvers can be very effective for this kind of problem, with run-time scaling linear with the number of unknowns. \n", + "**Note** Simpler but theoretically worse scaling methods can be faster for small mesh sizes $N$.\n", + "\n", + "To do this we need to:\n", + "- Generate a matrix that represents the $\\Delta^*$ operator on each mesh resolution: https://github.com/freegs-plasma/freegs/blob/master/freegs/gradshafranov.py#L153\n", + "- If using multiple resolution levels, generate a matrix for each level: https://github.com/freegs-plasma/freegs/blob/master/freegs/multigrid.py#L139\n", + "- At the coarsest level create a direct solver (using LU decomposition in SciPy): https://github.com/freegs-plasma/freegs/blob/master/freegs/multigrid.py#L35" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define a toroidal current density\n", + "J_tor = np.exp(-(R - 0.5)**2 - Z**2) * 1e3 # In A/m^2\n", + "\n", + "# Matrix generator, giving range of R and Z we're going to solve over\n", + "from freegs.gradshafranov import GSsparse\n", + "generator = GSsparse(np.amin(R), np.amax(R), np.amin(Z), np.amax(Z))\n", + "\n", + "# For example a 3x3 mesh. 2nd order method has 5-point stencil\n", + "np.set_printoptions(precision=2)\n", + "generator(3,3).toarray()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Generate a matrix for our array\n", + "nx, ny = R.shape\n", + "A = generator(nx, ny)\n", + "\n", + "# LU factorize (https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.linalg.factorized.html)\n", + "from scipy.sparse.linalg import factorized\n", + "solver = factorized(A.tocsc())\n", + "\n", + "# Solve for psi from J_tor\n", + "mu0 = 4e-7 * np.pi\n", + "psi = solver((mu0 * R * J_tor).flatten()).reshape(R.shape)\n", + "\n", + "# Plot contours of psi\n", + "plt.contour(R, Z, psi, 20)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The boundaries look wrong (they *are* wrong): In the 3x3 matrix above the boundary cells have `1` on the diagonal. That means the boundary values of $\\psi$ are set to the boundary cells of the RHS, i.e. $\\mu_0 R J_\\phi$.\n", + "\n", + "One approximation is to set boundaries to conducting: $\\psi = $ constant on the boundary" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "rhs = -mu0 * R * J_tor\n", + "# Set all boundaries of psi to zero:\n", + "rhs[0,:] = rhs[-1,:] = rhs[:,0] = rhs[:,-1] = 0\n", + "\n", + "# Re-solve\n", + "psi = solver(rhs.flatten()).reshape(R.shape)\n", + "\n", + "# Plot contours of psi\n", + "plt.contour(R, Z, psi, 20)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Free boundary solutions\n", + "\n", + "To obtain the boundary conditions for a \"free\" boundary, one way is to use the brute-force approach (integrate Green's functions) along the boundary.\n", + "https://github.com/freegs-plasma/freegs/blob/master/freegs/boundary.py#L50\n", + "\n", + "For each point on the boundary we perform an integral over the 2D $(R,Z)$ domain.\n", + "\n", + "Romberg integration is an accurate method, but needs $2^n + 1$ points. Hence grid sizes like 33, 65, 129.\n", + "\n", + "Note: \n", + "- This is more efficient than a full brute force, because integrals are only for the boundary points rather than every point in the domain\n", + "- A more effient method is von Hagenow's method. That replaces 2D integrals with a calculation of normal derivatives, and a 1D integral over the boundary. The default method in FreeGS: https://github.com/freegs-plasma/freegs/blob/master/freegs/boundary.py#L102" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "rhs = -mu0 * R * J_tor\n", + "\n", + "# List of indices on the boundary\n", + "bndry_indices = np.concatenate(\n", + " [\n", + " [(x, 0) for x in range(nx)],\n", + " [(x, ny - 1) for x in range(nx)],\n", + " [(0, y) for y in range(ny)],\n", + " [(nx - 1, y) for y in range(ny)],\n", + " ]\n", + ")\n", + "\n", + "from freegs.gradshafranov import Greens\n", + "from scipy.integrate import romb\n", + "\n", + "dR = R[1, 0] - R[0, 0]\n", + "dZ = Z[0, 1] - Z[0, 0]\n", + "\n", + "for x, y in bndry_indices:\n", + " # Calculate the response of the boundary point\n", + " # to each cell in the plasma domain\n", + " greenfunc = Greens(R, Z, R[x, y], Z[x, y])\n", + "\n", + " # Prevent infinity/nan by removing (x,y) point\n", + " greenfunc[x, y] = 0.0\n", + " \n", + " # Integrate over the domain\n", + " rhs[x, y] = romb(romb(greenfunc * J_tor)) * dR * dZ\n", + " \n", + "# Re-solve\n", + "psi = solver(rhs.flatten()).reshape(R.shape)\n", + "\n", + "# Plot contours of psi\n", + "plt.contour(R, Z, psi, 20)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Radial motion\n", + "\n", + "Plotting the toroidal current $J_\\phi$ on top of the contours of $\\psi$ we will see that they don't line up.\n", + "The poloidal flux $\\psi$ is shifted radially outwards." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plt.contour(R, Z, J_tor, 30) # Coloured lines are toroidal current\n", + "plt.contour(R, Z, psi, 20, colors='k') # Black contours are poloidal flux" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In a static plasma the current flows on flux surfaces (cross-field currents = torques). If we now try to update the solution, putting our toroidal current on flux surfaces, the plasma will have moved outwards!\n", + "\n", + "**A toroidal current-carrying plasma will expand radially outwards**\n", + "\n", + "This is seen in toroidal force balance as the **Hoop force** and **Tire force**. It appears in free-boundary Grad-Shafranov solvers as a systematic shift between iterations, since here we are not considering plasma inertia.\n", + "\n", + "\n", + "# Radial force balance\n", + "\n", + "The solution is to add a vertical magnetic field, providing an inward force. We can vary the current in the coil to find the coil current that keeps the plasma in (approximately) the initial location." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "circuit_1 = Circuit([(\"P1L\", Coil(1.0, -1.0), 1.0),\n", + " (\"P1U\", Coil(1.0, 1.0), 1.0)], # In series => Vertical field\n", + " current = -1e3) # 1kA in this circuit\n", + "\n", + "# Add plasma and coil psi\n", + "total_psi = psi + circuit_1.psi(R, Z)\n", + "\n", + "# Plot toroidal current and total psi\n", + "plt.contour(R, Z, J_tor, 30) # Coloured lines are toroidal current\n", + "plt.contour(R, Z, total_psi, 40, colors='k') # Black contours are poloidal flux" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is what the control system in a free-boundary Grad-Shafranov solver does!\n", + "\n", + "Given a set of coils:\n", + "- The user may fix some currents\n", + "- Constraints are provided by the user: Locations of X-points ($B_R = B_Z = 0$) and iso-flux i.e. two points with the same value of $\\psi$\n", + "- An automatic control system tries to find combinations of coil currents that best match the constraints\n", + "\n", + "This is typically an ill-posed problem, either too many constraints or too few. \n", + "- Regularisation needed\n", + "- Typically also want to minimise coil currents\n", + "\n", + "**Note**: The method used in FreeGS minimizes the *change* in coil current between iterations, not the coil current. The coil currents it finds **may not be a global optimum**: https://github.com/freegs-plasma/freegs/blob/master/freegs/control.py#L68\n", + "- Other control methods may be used in practice to find global optimum" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Example of setting up a machine\n", + "\n", + "1. A definition of the locations of the poloidal field coils, and how they are wired together, for example connected in series to the same power supplies. This is specific to the machine.\n", + "2. Plasma profiles of pressure and current, p(psi) and f(psi), together with global contraints such as total plasma current, which indirectly specify the size of the plasma.\n", + "3. A control system which sets the shape and location of the plasma. Like a real plasma, feedback control is needed to stabilise vertical and radial motion in free-boundary Grad-Shafranov solvers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Step 1: Specify the locations of the coils, and the domain you want to solve over\n", + "\n", + "from freegs import machine\n", + "from freegs.equilibrium import Equilibrium\n", + "\n", + "# Define the poloidal field coil set\n", + "tokamak = machine.MAST()\n", + "\n", + "# Define the domain to solve over\n", + "eq = Equilibrium(tokamak=tokamak,\n", + " Rmin=0.1, Rmax=2.0, # Radial domain\n", + " Zmin=-2.0, Zmax=2.0, # Height range\n", + " nx=65, ny=65) # Number of grid points" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Step 2: Specify the profiles of pressure and f=R*Bt. \n", + "# Currently quite simple functions are supported\n", + "\n", + "from freegs.jtor import ConstrainPaxisIp\n", + "\n", + "profiles = ConstrainPaxisIp(eq, # Equilibrium\n", + " 3e3, # Plasma pressure on axis [Pascals]\n", + " 7e5, # Plasma current [Amps]\n", + " 0.4) # vacuum f = R*Bt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Step 3: Specify the control system and feedback variables.\n", + "# The control system adjusts the currents in the poloidal field coils\n", + "# to produce X-points in the desired locations, and ensure that the desired\n", + "# pairs of locations have the same poloidal flux.\n", + "\n", + "from freegs import control\n", + "\n", + "xpoints = [(0.7, -1.1), # (R,Z) locations of X-points\n", + " (0.7, 1.1)]\n", + "\n", + "# Contstrain these pairs of (R,Z, R,Z) locations to have the same poloidal flux\n", + "# This is needed for radial and vertical position control of the plasma.\n", + "isoflux = [(0.7,-1.1, 1.45, 0.0) # Lower X-point, Outboard midplane\n", + " ,(0.7,1.1, 1.45, 0.0) # Upper X-point, Outboard midplane\n", + " ]\n", + "\n", + "constrain = control.constrain(xpoints=xpoints, gamma=1e-12, isoflux=isoflux)\n", + "\n", + "constrain(eq)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# With these three components (coils, profiles and constraints), solve the nonlinear\n", + "# system with a Picard iteration. This modifies the \"eq\" object.\n", + "\n", + "from freegs import picard\n", + "\n", + "picard.solve(eq, # The equilibrium to adjust\n", + " profiles, # The toroidal current profile function\n", + " constrain) # Constraint function to set coil currents" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Plasma current: %e Amps\" % (eq.plasmaCurrent()))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tokamak.printCurrents()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from freegs.plotting import plotEquilibrium\n", + "plotEquilibrium(eq)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Modifying the equilibrium\n", + "-------------------------\n", + "\n", + "Modify the constraints for the X-point locations and isoflux pairs. Starting from the previous solution, this quite quickly finds a new solution." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "xpoints = [(0.7, -1.0), # (R,Z) locations of X-points\n", + " (0.7, 1.0)]\n", + "\n", + "isoflux = [(0.7,-1.0, 1.4, 0.0),(0.7,1.0, 1.4, 0.0), (0.7,-1.0, 0.3, 0.0)]\n", + "\n", + "constrain = control.constrain(xpoints=xpoints, gamma=1e-12, isoflux=isoflux)\n", + "\n", + "constrain(eq)\n", + "\n", + "plotEquilibrium(eq)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "picard.solve(eq, # The equilibrium to adjust\n", + " profiles, # The toroidal current profile function\n", + " constrain) # Constraint function to set coil currents" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plotEquilibrium(eq)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +}