|
5 | 5 | import numpy as np
|
6 | 6 |
|
7 | 7 | from desc.grid import Grid, LinearGrid, QuadratureGrid
|
8 |
| -from desc.utils import errorif |
| 8 | +from desc.profiles import FourierZernikeProfile, PowerSeriesProfile, SplineProfile |
| 9 | +from desc.utils import errorif, setdefault, warnif |
9 | 10 |
|
10 | 11 |
|
11 | 12 | def ensure_positive_jacobian(eq):
|
@@ -330,3 +331,135 @@ def _get_new_coeffs(fun):
|
330 | 331 | eq_rotated.axis = eq_rotated.get_axis()
|
331 | 332 |
|
332 | 333 | return eq_rotated
|
| 334 | + |
| 335 | + |
| 336 | +def contract_equilibrium( |
| 337 | + eq, inner_rho, contract_profiles=True, profile_num_points=None, copy=True |
| 338 | +): |
| 339 | + """Contract an equilibrium so that an inner flux surface is the new boundary. |
| 340 | +
|
| 341 | + Parameters |
| 342 | + ---------- |
| 343 | + eq : Equilibrium |
| 344 | + Equilibrium to contract. |
| 345 | + inner_rho: float |
| 346 | + rho value (<1) to contract the Equilibrium to. |
| 347 | + contract_profiles : bool |
| 348 | + Whether or not to contract the profiles. |
| 349 | + If True, the new profile's value at ``rho=1.0`` will be the same as the old |
| 350 | + profile's value at ``rho=inner_rho``, i.e. in physical space, the new |
| 351 | + profile is the same as the old profile. If the profile is a |
| 352 | + ``PowerSeriesProfile`` or ``SplineProfile``, the same profile type will be |
| 353 | + returned. If not one of these two classes, a ``SplineProfile`` will be returned |
| 354 | + as other profile classes cannot be safely contracted. |
| 355 | + If False, the new profile will have the same functional form |
| 356 | + as the old profile, with no rescaling performed. This means the new equilibrium |
| 357 | + has a physically different profile than the original equilibrium. |
| 358 | + profile_num_points : int |
| 359 | + Number of points to use when fitting or re-scaling the profiles. Defaults to |
| 360 | + ``eq.L_grid`` |
| 361 | + copy : bool |
| 362 | + Whether or not to return a copy or to modify the original equilibrium. |
| 363 | +
|
| 364 | + Returns |
| 365 | + ------- |
| 366 | + eq_inner: New Equilibrium object, contracted from the old one such that |
| 367 | + eq.pressure(rho=inner_rho) = eq_inner.pressure(rho=1), and |
| 368 | + eq_inner LCFS = eq's rho=inner_rho surface. |
| 369 | + Note that this will not be in force balance, and so must be re-solved. |
| 370 | +
|
| 371 | + """ |
| 372 | + errorif( |
| 373 | + not (inner_rho < 1 and inner_rho > 0), |
| 374 | + ValueError, |
| 375 | + f"Surface must be in the range 0 < inner_rho < 1, instead got {inner_rho}.", |
| 376 | + ) |
| 377 | + profile_num_points = setdefault(profile_num_points, eq.L_grid) |
| 378 | + |
| 379 | + def scale_profile(profile, rho): |
| 380 | + errorif( |
| 381 | + isinstance(profile, FourierZernikeProfile), |
| 382 | + ValueError, |
| 383 | + "contract_equilibrium does not support FourierZernikeProfile", |
| 384 | + ) |
| 385 | + if profile is None: |
| 386 | + return profile |
| 387 | + is_power_series = isinstance(profile, PowerSeriesProfile) |
| 388 | + if contract_profiles and is_power_series: |
| 389 | + # only PowerSeriesProfile both |
| 390 | + # a) has a from_values |
| 391 | + # b) can safely use that from_values to represent a |
| 392 | + # subset of itself. |
| 393 | + x = np.linspace(0, 1, profile_num_points) |
| 394 | + grid = LinearGrid(rho=x / rho) |
| 395 | + y = profile.compute(grid) |
| 396 | + return profile.from_values(x=x, y=y) |
| 397 | + elif contract_profiles: |
| 398 | + warnif( |
| 399 | + not isinstance(profile, SplineProfile), |
| 400 | + UserWarning, |
| 401 | + f"{profile} is not a PowerSeriesProfile or SplineProfile," |
| 402 | + " so cannot safely contract using the same profile type." |
| 403 | + "Falling back to fitting the values with a SplineProfile", |
| 404 | + ) |
| 405 | + x = np.linspace(0, 1, profile_num_points) |
| 406 | + grid = LinearGrid(rho=x / rho) |
| 407 | + y = profile.compute(grid) |
| 408 | + return SplineProfile(knots=x, values=y) |
| 409 | + else: # don't do any scaling of the profile |
| 410 | + return profile |
| 411 | + |
| 412 | + # create new profiles for contracted equilibrium |
| 413 | + pressure = scale_profile(eq.pressure, inner_rho) |
| 414 | + iota = scale_profile(eq.iota, inner_rho) |
| 415 | + current = scale_profile(eq.current, inner_rho) |
| 416 | + electron_density = scale_profile(eq.electron_density, inner_rho) |
| 417 | + electron_temperature = scale_profile(eq.electron_temperature, inner_rho) |
| 418 | + ion_temperature = scale_profile(eq.ion_temperature, inner_rho) |
| 419 | + atomic_number = scale_profile(eq.atomic_number, inner_rho) |
| 420 | + anisotropy = scale_profile(eq.anisotropy, inner_rho) |
| 421 | + |
| 422 | + surf_inner = eq.get_surface_at(rho=inner_rho) |
| 423 | + surf_inner.rho = 1.0 |
| 424 | + from .equilibrium import Equilibrium |
| 425 | + |
| 426 | + eq_inner = Equilibrium( |
| 427 | + surface=surf_inner, |
| 428 | + pressure=pressure, |
| 429 | + iota=iota, |
| 430 | + current=current, |
| 431 | + electron_density=electron_density, |
| 432 | + electron_temperature=electron_temperature, |
| 433 | + ion_temperature=ion_temperature, |
| 434 | + atomic_number=atomic_number, |
| 435 | + anisotropy=anisotropy, |
| 436 | + Psi=( |
| 437 | + eq.Psi * inner_rho**2 |
| 438 | + ), # flux (in Webers) within the new last closed flux surface |
| 439 | + NFP=eq.NFP, |
| 440 | + L=eq.L, |
| 441 | + M=eq.M, |
| 442 | + N=eq.N, |
| 443 | + L_grid=eq.L_grid, |
| 444 | + M_grid=eq.M_grid, |
| 445 | + N_grid=eq.N_grid, |
| 446 | + sym=eq.sym, |
| 447 | + ensure_nested=False, # we fit the surfaces later so don't check now |
| 448 | + ) |
| 449 | + inner_grid = LinearGrid( |
| 450 | + rho=np.linspace(0, inner_rho, eq.L_grid * 2), |
| 451 | + M=eq.M_grid, |
| 452 | + N=eq.N_grid, |
| 453 | + NFP=eq.NFP, |
| 454 | + axis=True, |
| 455 | + ) |
| 456 | + inner_data = eq.compute(["R", "Z", "lambda"], grid=inner_grid) |
| 457 | + nodes = inner_grid.nodes |
| 458 | + nodes[:, 0] = nodes[:, 0] / inner_rho |
| 459 | + eq_inner.set_initial_guess( |
| 460 | + nodes, inner_data["R"], inner_data["Z"], inner_data["lambda"] |
| 461 | + ) |
| 462 | + if not copy: # overwrite the original eq |
| 463 | + eq.__dict__.update(eq_inner.__dict__) |
| 464 | + return eq |
| 465 | + return eq_inner |
0 commit comments