-
Notifications
You must be signed in to change notification settings - Fork 27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Integrate on boundary to compute length scale quantities #1094
base: master
Are you sure you want to change the base?
Changes from 9 commits
56ead44
504cdfc
27a6fcb
f1c7634
2694e8c
69fa9ce
93c710b
7970177
be11015
80feb38
c31abcd
acf0486
9974137
3ea229d
1f6c8ce
8c778fa
5912c79
1659fc0
4222b74
cf81a3c
1bc81ae
b6bb3ce
167156f
780ddb3
a93b98b
66b402a
01e212f
d7c6d26
3fb1ccf
c21913e
6adaa96
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,9 +9,11 @@ | |
expensive computations. | ||
""" | ||
|
||
from desc.backend import jnp | ||
from desc.backend import jnp, trapezoid | ||
|
||
from ..grid import QuadratureGrid | ||
from .data_index import register_compute_fun | ||
from .geom_utils import warnif_sym | ||
from .utils import cross, dot, line_integrals, safenorm, surface_integrals | ||
|
||
|
||
|
@@ -26,11 +28,16 @@ | |
transforms={"grid": []}, | ||
profiles=[], | ||
coordinates="", | ||
data=["sqrt(g)"], | ||
data=["sqrt(g)", "V(r)", "rho"], | ||
resolution_requirement="rtz", | ||
) | ||
def _V(params, transforms, profiles, data, **kwargs): | ||
data["V"] = jnp.sum(data["sqrt(g)"] * transforms["grid"].weights) | ||
if isinstance(transforms["grid"], QuadratureGrid): | ||
data["V"] = jnp.sum(data["sqrt(g)"] * transforms["grid"].weights) | ||
else: | ||
# To approximate volume at ρ ~ 1, we scale by ρ⁻², assuming the integrand | ||
# varies little from ρ = max_rho to ρ = 1 and a roughly circular cross-section. | ||
data["V"] = jnp.max(data["V(r)"]) / jnp.max(data["rho"]) ** 2 | ||
return data | ||
|
||
|
||
|
@@ -39,27 +46,19 @@ def _V(params, transforms, profiles, data, **kwargs): | |
label="V", | ||
units="m^{3}", | ||
units_long="cubic meters", | ||
description="Volume", | ||
description="Volume enclosed by surface, scaled by max(ρ)⁻²", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. idk if this docstring is clear enough, something like "if grid does not contain rho=1" or something could possibly help? |
||
dim=1, | ||
params=[], | ||
transforms={"grid": []}, | ||
profiles=[], | ||
coordinates="", | ||
data=["e_theta", "e_zeta", "x"], | ||
data=["V(r)", "rho"], | ||
parameterization="desc.geometry.surface.FourierRZToroidalSurface", | ||
resolution_requirement="tz", | ||
) | ||
def _V_FourierRZToroidalSurface(params, transforms, profiles, data, **kwargs): | ||
# divergence theorem: integral(dV div [0, 0, Z]) = integral(dS dot [0, 0, Z]) | ||
data["V"] = jnp.max( # take max in case there are multiple surfaces for some reason | ||
jnp.abs( | ||
surface_integrals( | ||
transforms["grid"], | ||
cross(data["e_theta"], data["e_zeta"])[:, 2] * data["x"][:, 2], | ||
expand_out=False, | ||
) | ||
) | ||
) | ||
# To approximate volume at ρ ~ 1, we scale by ρ⁻², assuming the integrand | ||
# varies little from ρ = max_rho to ρ = 1 and a roughly circular cross-section. | ||
data["V"] = jnp.max(data["V(r)"]) / jnp.max(data["rho"]) ** 2 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to do this scaling here? IE, if the user creates a surface with rho=0.7, do we want to compute the actual volume? or the extrapolated volume? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a good point, if I want an interior volume this would still extrapolate. Maybe there should be a "V_full" key that does extrapolation and a "V" that just gives the volume? I guess that is just an alias for V(r) though |
||
return data | ||
|
||
|
||
|
@@ -75,6 +74,10 @@ def _V_FourierRZToroidalSurface(params, transforms, profiles, data, **kwargs): | |
profiles=[], | ||
coordinates="r", | ||
data=["e_theta", "e_zeta", "Z"], | ||
parameterization=[ | ||
"desc.equilibrium.equilibrium.Equilibrium", | ||
"desc.geometry.surface.FourierRZToroidalSurface", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using this for |
||
], | ||
resolution_requirement="tz", | ||
) | ||
def _V_of_r(params, transforms, profiles, data, **kwargs): | ||
|
@@ -155,45 +158,20 @@ def _V_rrr_of_r(params, transforms, profiles, data, **kwargs): | |
label="A(\\zeta)", | ||
units="m^{2}", | ||
units_long="square meters", | ||
description="Cross-sectional area as function of zeta", | ||
description="Area of enclosed cross-section (enclosed constant phi surface), " | ||
"scaled by max(ρ)⁻², as function of zeta", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same point on clarity as above |
||
dim=1, | ||
params=[], | ||
transforms={"grid": []}, | ||
profiles=[], | ||
coordinates="z", | ||
data=["|e_rho x e_theta|"], | ||
data=["Z", "n_rho", "e_theta|r,p", "rho"], | ||
parameterization=[ | ||
"desc.equilibrium.equilibrium.Equilibrium", | ||
"desc.geometry.surface.ZernikeRZToroidalSection", | ||
"desc.geometry.surface.FourierRZToroidalSurface", | ||
], | ||
resolution_requirement="rt", | ||
) | ||
def _A_of_z(params, transforms, profiles, data, **kwargs): | ||
data["A(z)"] = surface_integrals( | ||
unalmis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
transforms["grid"], | ||
data["|e_rho x e_theta|"], | ||
surface_label="zeta", | ||
expand_out=True, | ||
) | ||
return data | ||
|
||
|
||
@register_compute_fun( | ||
name="A(z)", | ||
label="A(\\zeta)", | ||
units="m^{2}", | ||
units_long="square meters", | ||
description="Area of enclosed cross-section (enclosed constant phi surface), " | ||
"scaled by max(ρ)⁻², as function of zeta", | ||
dim=1, | ||
params=[], | ||
transforms={"grid": []}, | ||
profiles=[], | ||
coordinates="z", | ||
data=["Z", "n_rho", "e_theta|r,p", "rho"], | ||
parameterization=["desc.geometry.surface.FourierRZToroidalSurface"], | ||
resolution_requirement="rt", # just need max(rho) near 1 | ||
# FIXME: Add source grid requirement once omega is nonzero. | ||
resolution_requirement="t", | ||
) | ||
def _A_of_z_FourierRZToroidalSurface(params, transforms, profiles, data, **kwargs): | ||
# Denote any vector v = [vᴿ, v^ϕ, vᶻ] with a tuple of its contravariant components. | ||
|
@@ -204,6 +182,7 @@ def _A_of_z_FourierRZToroidalSurface(params, transforms, profiles, data, **kwarg | |
# where n is the unit normal such that n dot e_θ|ρ,ϕ = 0 and n dot e_ϕ|R,Z = 0, | ||
# and the labels following | denote those coordinates are fixed. | ||
# Now choose v = [0, 0, Z], and n in the direction (e_θ|ρ,ζ × e_ζ|ρ,θ) ⊗ [1, 0, 1]. | ||
warnif_sym(transforms["grid"], "A(z)") | ||
n = data["n_rho"] | ||
n = n.at[:, 1].set(0) | ||
n = n / jnp.linalg.norm(n, axis=-1)[:, jnp.newaxis] | ||
|
@@ -232,23 +211,46 @@ def _A_of_z_FourierRZToroidalSurface(params, transforms, profiles, data, **kwarg | |
label="A", | ||
units="m^{2}", | ||
units_long="square meters", | ||
description="Average cross-sectional area", | ||
description="Average enclosed cross-sectional area, scaled by max(ρ)⁻²", | ||
dim=0, | ||
params=[], | ||
transforms={"grid": []}, | ||
profiles=[], | ||
coordinates="", | ||
data=["A(z)"], | ||
data=["Z", "n_rho", "e_theta|r,p", "rho", "phi"], | ||
parameterization=[ | ||
"desc.equilibrium.equilibrium.Equilibrium", | ||
"desc.geometry.core.Surface", | ||
"desc.equilibrium.equilibrium.Equilibrium", | ||
], | ||
resolution_requirement="z", | ||
resolution_requirement="tz", | ||
) | ||
def _A(params, transforms, profiles, data, **kwargs): | ||
data["A"] = jnp.mean( | ||
transforms["grid"].compress(data["A(z)"], surface_label="zeta") | ||
# Denote any vector v = [vᴿ, v^ϕ, vᶻ] with a tuple of its contravariant components. | ||
# We use a 2D divergence theorem over constant ϕ toroidal surface (i.e. R, Z plane). | ||
# In this geometry, the divergence operator on a polar basis vector is | ||
# div = ([∂_R, ∂_ϕ, ∂_Z] ⊗ [1, 0, 1]) dot . | ||
unalmis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# ∫ dA div v = ∫ dℓ n dot v | ||
# where n is the unit normal such that n dot e_θ|ρ,ϕ = 0 and n dot e_ϕ|R,Z = 0, | ||
# and the labels following | denote those coordinates are fixed. | ||
# Now choose v = [0, 0, Z], and n in the direction (e_θ|ρ,ζ × e_ζ|ρ,θ) ⊗ [1, 0, 1]. | ||
n = data["n_rho"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. iirc because n_rho should not be defined on zeta cross section (#1127 ) the task is to rewrite this to obtain normal vector purely from e_theta which is defined on zeta cross section alone. basically like #597 (comment). |
||
n = n.at[:, 1].set(0) | ||
n = n / jnp.linalg.norm(n, axis=-1)[:, jnp.newaxis] | ||
max_rho = jnp.max(data["rho"]) | ||
A = jnp.abs( | ||
line_integrals( | ||
transforms["grid"], | ||
data["Z"] * n[:, 2] * safenorm(data["e_theta|r,p"], axis=-1), | ||
line_label="theta", | ||
fix_surface=("rho", max_rho), | ||
expand_out=False, | ||
) | ||
# To approximate area at ρ ~ 1, we scale by ρ⁻², assuming the integrand | ||
# varies little from ρ = max_rho to ρ = 1 and a roughly circular cross-section. | ||
/ max_rho**2 | ||
) | ||
phi = transforms["grid"].compress(data["phi"], "zeta") | ||
data["A"] = jnp.squeeze(trapezoid(A, phi) / jnp.ptp(phi) if A.size > 1 else A) | ||
return data | ||
|
||
|
||
|
@@ -275,25 +277,22 @@ def _A_of_r(params, transforms, profiles, data, **kwargs): | |
label="S", | ||
units="m^{2}", | ||
units_long="square meters", | ||
description="Surface area of outermost flux surface", | ||
description="Surface area of outermost flux surface, scaled by max(ρ)⁻¹", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you referring to the docstring saying scaled by max rho? I think so There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but I also changed the compute function so that we scale by max rho. Do we want to do that? |
||
dim=0, | ||
params=[], | ||
transforms={"grid": []}, | ||
profiles=[], | ||
coordinates="", | ||
data=["|e_theta x e_zeta|"], | ||
data=["S(r)", "rho"], | ||
parameterization=[ | ||
"desc.equilibrium.equilibrium.Equilibrium", | ||
"desc.geometry.surface.FourierRZToroidalSurface", | ||
], | ||
resolution_requirement="tz", | ||
) | ||
def _S(params, transforms, profiles, data, **kwargs): | ||
data["S"] = jnp.max( | ||
surface_integrals( | ||
transforms["grid"], data["|e_theta x e_zeta|"], expand_out=False | ||
) | ||
) | ||
# To approximate surface are at ρ ~ 1, we scale by ρ⁻¹, assuming the integrand | ||
# varies little from ρ = max_rho to ρ = 1. | ||
data["S"] = jnp.max(data["S(r)"]) / jnp.max(data["rho"]) | ||
return data | ||
|
||
|
||
|
@@ -309,6 +308,10 @@ def _S(params, transforms, profiles, data, **kwargs): | |
profiles=[], | ||
coordinates="r", | ||
data=["|e_theta x e_zeta|"], | ||
parameterization=[ | ||
"desc.equilibrium.equilibrium.Equilibrium", | ||
"desc.geometry.surface.FourierRZToroidalSurface", | ||
], | ||
resolution_requirement="tz", | ||
) | ||
def _S_of_r(params, transforms, profiles, data, **kwargs): | ||
|
@@ -440,9 +443,10 @@ def _R0_over_a(params, transforms, profiles, data, **kwargs): | |
"desc.equilibrium.equilibrium.Equilibrium", | ||
"desc.geometry.core.Surface", | ||
], | ||
resolution_requirement="rt", # just need max(rho) near 1 | ||
resolution_requirement="t", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. otherwise, we get unnecessary warnings when using a grid with rho=1 surface |
||
) | ||
def _perimeter_of_z(params, transforms, profiles, data, **kwargs): | ||
warnif_sym(transforms["grid"], "perimeter(z)") | ||
max_rho = jnp.max(data["rho"]) | ||
data["perimeter(z)"] = ( | ||
line_integrals( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,11 +2,40 @@ | |
|
||
import functools | ||
|
||
from termcolor import colored | ||
|
||
from desc.backend import jnp | ||
|
||
from ..utils import ResolutionWarning, errorif, warnif | ||
from .utils import safenorm, safenormalize | ||
|
||
|
||
def warnif_sym(grid, name): | ||
"""Warn if grid has truncated poloidal domain to [0, π] ⊂ [0, 2π).""" | ||
warnif( | ||
grid.sym, | ||
ResolutionWarning, | ||
msg=colored( | ||
"This grid only samples the poloidal domain θ ∈ [0, π], " | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should mention stellarator symmetry in the warning as well (this grid has stellarator sym, which means it only samples...) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe also say that a grid with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the first note, in a related issue mentioned at top, I mention the goal
So I don't want to refer to grid as having stellarator symmetry. |
||
f"but, in general, the full domain [0, 2π) is required to compute {name}.", | ||
"yellow", | ||
), | ||
) | ||
|
||
|
||
def errorif_sym(grid, name): | ||
"""Warn if grid has truncated poloidal domain to [0, π] ⊂ [0, 2π).""" | ||
errorif( | ||
grid.sym, | ||
ResolutionWarning, | ||
msg=colored( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto above comment |
||
"This grid only samples the poloidal domain θ ∈ [0, π], " | ||
f"but, in general, the full domain [0, 2π) is required to compute {name}.", | ||
"yellow", | ||
), | ||
) | ||
|
||
|
||
def reflection_matrix(normal): | ||
"""Matrix to reflect points across plane through origin with specified normal. | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we include logic like this for some of the other cases? My main worry is that for higher order quantities that depend on V,A,a etc we're making it so that you need multiple different grids just to compute everything correctly, but i think in theory a quadrature grid should work for everything (though may be overkill in some cases)