Skip to content
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

Draft
wants to merge 31 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
56ead44
Merge branch 'fieldline_compute' into integrate_on_boundary
unalmis Jul 2, 2024
504cdfc
Merge branch 'fieldline_compute' into integrate_on_boundary
unalmis Jul 4, 2024
27a6fcb
Update master compute data with follog differences:
unalmis Jul 4, 2024
f1c7634
Add scaling by max(rho) information to description
unalmis Jul 4, 2024
2694e8c
Update master_compute_data from master
unalmis Jul 5, 2024
69fa9ce
Merge branch 'fieldline_compute' into integrate_on_boundary
unalmis Jul 5, 2024
93c710b
Remove unneeded resolution requirement from divergence theorems compu…
unalmis Jul 5, 2024
7970177
Merge branch 'fieldline_compute' into integrate_on_boundary
unalmis Jul 10, 2024
be11015
Make changes in light of https://github.com/PlasmaControl/DESC/issues…
unalmis Jul 11, 2024
80feb38
Merge branch 'fieldline_compute' into integrate_on_boundary
unalmis Jul 11, 2024
c31abcd
Fix math error in comment
unalmis Jul 12, 2024
acf0486
Update master compute data xyz: following changes
unalmis Jul 13, 2024
9974137
Merge branch 'fieldline_compute' into integrate_on_boundary
ddudt Jul 15, 2024
3ea229d
Merge branch 'master' into integrate_on_boundary
unalmis Jul 20, 2024
1f6c8ce
Loosen test tolerance for vmec comparison test for length scale quant…
unalmis Jul 20, 2024
8c778fa
Remove changes to adding_compute_funs.rst
unalmis Jul 20, 2024
5912c79
Fix bug in plot_1d
unalmis Jul 20, 2024
1659fc0
Fix test by suppressing warning
unalmis Jul 21, 2024
4222b74
Fix unrecognized char error since warnings doesn't recognize unicode
unalmis Jul 21, 2024
cf81a3c
Fix test that assumes S = S(r) instead of outermost
unalmis Jul 21, 2024
1bc81ae
Merge branch 'master' into integrate_on_boundary
unalmis Aug 21, 2024
b6bb3ce
Merge branch 'master' into integrate_on_boundary
unalmis Sep 4, 2024
167156f
Merge branch 'master' into integrate_on_boundary
unalmis Sep 16, 2024
780ddb3
clean up comment
unalmis Sep 16, 2024
a93b98b
Remove old code
unalmis Sep 17, 2024
66b402a
Merge branch 'master' into integrate_on_boundary
unalmis Sep 19, 2024
01e212f
Merge branch 'master' into integrate_on_boundary
unalmis Sep 30, 2024
d7c6d26
Merge branch 'master' into integrate_on_boundary
unalmis Oct 6, 2024
3fb1ccf
Merge branch 'master' into integrate_on_boundary
unalmis Oct 17, 2024
c21913e
Merge branch 'master' into integrate_on_boundary
unalmis Oct 18, 2024
6adaa96
Using grid requirement instead of warnings
unalmis Oct 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 87 additions & 74 deletions desc/compute/_geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
expensive computations.
"""

from desc.backend import jnp
from desc.backend import jnp, trapezoid

from ..grid import QuadratureGrid
from ..integrals.surface_integral import line_integrals, surface_integrals
from ..utils import cross, dot, safenorm
from .data_index import register_compute_fun
Expand All @@ -27,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):
Copy link
Member

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)

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 ρ to ρ = 1 and a roughly circular cross-section.
data["V"] = jnp.max(data["V(r)"]) / jnp.max(data["rho"]) ** 2

Check warning on line 40 in desc/compute/_geometry.py

View check run for this annotation

Codecov / codecov/patch

desc/compute/_geometry.py#L40

Added line #L40 was not covered by tests
return data


Expand All @@ -40,27 +46,19 @@
label="V",
units="m^{3}",
units_long="cubic meters",
description="Volume",
description="Volume enclosed by surface, scaled by max(ρ)⁻²",
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 ρ to ρ = 1 and a roughly circular cross-section.
data["V"] = jnp.max(data["V(r)"]) / jnp.max(data["rho"]) ** 2
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Collaborator

Choose a reason for hiding this comment

The 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


Expand All @@ -76,6 +74,10 @@
profiles=[],
coordinates="r",
data=["e_theta", "e_zeta", "Z"],
parameterization=[
"desc.equilibrium.equilibrium.Equilibrium",
"desc.geometry.surface.FourierRZToroidalSurface",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using this for FourierRZToroidalSurface feels a bit weird since in that case its not really a funciton of rho.

],
resolution_requirement="tz",
)
def _V_of_r(params, transforms, profiles, data, **kwargs):
Expand Down Expand Up @@ -156,50 +158,31 @@
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",
Copy link
Collaborator

Choose a reason for hiding this comment

The 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",
resolution_requirement="t",
grid_requirement={"sym": False},
# FIXME: For nonzero omega we need to integrate over theta at constant phi.
# Add source_grid_requirement={"coordinates": "rtp", "is_meshgrid": True}
# TODO: Recognize when omega = 0 and ignore all source grid requirements
# if the given grid satisfies them with phi replaced by zeta.
)
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.
)
def _A_of_z_FourierRZToroidalSurface(params, transforms, profiles, data, **kwargs):
# Denote any vector v = [vᴿ, v^ϕ, vᶻ] with a tuple of its contravariant components.
# Denote any vector v = v¹ R̂ + v² ϕ̂ + v³ Ẑ by v = [v¹, v², v³] where R̂, ϕ̂, Ẑ
# are the normalized basis vectors of the cylindrical coordinates R, ϕ, Z.
# 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
# In this geometry, the divergence operator in this coordinate system is
# div = ([∂_R, ∂_ϕ, ∂_Z] ⊗ [1, 0, 1]) dot .
# ∫ 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,
Expand All @@ -213,16 +196,12 @@
line_integrals(
transforms["grid"],
data["Z"] * n[:, 2] * safenorm(data["e_theta|r,p"], axis=-1),
# FIXME: Works currently for omega = zero, but for nonzero omega
# we need to integrate over theta at constant phi.
# Should be simple once we have coordinate mapping and source grid
# logic from GitHub pull request #1024.
line_label="theta",
fix_surface=("rho", max_rho),
expand_out=True,
)
# To approximate area at ρ ~ 1, we scale by ρ⁻², assuming the integrand
# varies little from ρ = max_rho to ρ = 1 and a roughly circular cross-section.
# varies little from max ρ to ρ = 1 and a roughly circular cross-section.
/ max_rho**2
)
return data
Expand All @@ -233,23 +212,53 @@
label="A",
units="m^{2}",
units_long="square meters",
description="Average cross-sectional area",
description="Average enclosed cross-sectional area, scaled by max(ρ)⁻²",
# Simple toroidal average A₀ = ∫ A(ζ) dζ / (2π) matches the convention for the
# average major radius R₀ = ∫ R(ρ=0) dζ / (2π).
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",
# FIXME: For nonzero omega we need to integrate over theta at constant phi.
# Add source_grid_requirement={"coordinates": "rtp", "is_meshgrid": True}
# TODO: Recognize when omega = 0 and ignore all source grid requirements
# if the given grid satisfies them with phi replaced by zeta.
)
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¹ R̂ + v² ϕ̂ + v³ Ẑ by v = [v¹, v², v³] where R̂, ϕ̂, Ẑ
# are the normalized basis vectors of the cylindrical coordinates R, ϕ, Z.
# We use a 2D divergence theorem over constant ϕ toroidal surface (i.e. R, Z plane).
# In this geometry, the divergence operator in this coordinate system 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"]
Copy link
Collaborator Author

@unalmis unalmis Oct 2, 2024

Choose a reason for hiding this comment

The 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 ρ 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


Expand All @@ -276,25 +285,22 @@
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(ρ)⁻¹",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want this?

Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 ρ to ρ = 1.
data["S"] = jnp.max(data["S(r)"]) / jnp.max(data["rho"])
return data


Expand All @@ -310,6 +316,10 @@
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):
Expand Down Expand Up @@ -360,10 +370,12 @@

@register_compute_fun(
name="R0",
label="R_{0}",
label="R_{0} = V / (2\\pi A) = \\int R(\\rho=0) d\\zeta / (2\\pi)",
units="m",
units_long="meters",
description="Average major radius",
# This differs from the average value of R on the magnetic axis.
# R₀ ≠ 〈 R(ρ=0) 〉 = ∫ (R ‖e_ζ‖)(ρ=0) dζ / ∫ ‖e_ζ‖(ρ=0) dζ.
dim=0,
params=[],
transforms={},
Expand Down Expand Up @@ -441,24 +453,25 @@
"desc.equilibrium.equilibrium.Equilibrium",
"desc.geometry.core.Surface",
],
resolution_requirement="rt", # just need max(rho) near 1
resolution_requirement="t",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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

grid_requirement={"sym": False},
# FIXME: For nonzero omega we need to integrate over theta at constant phi.
# Add source_grid_requirement={"coordinates": "rtp", "is_meshgrid": True}
# TODO: Recognize when omega = 0 and ignore all source grid requirements
# if the given grid satisfies them with phi replaced by zeta.
)
def _perimeter_of_z(params, transforms, profiles, data, **kwargs):
max_rho = jnp.max(data["rho"])
data["perimeter(z)"] = (
line_integrals(
transforms["grid"],
safenorm(data["e_theta|r,p"], axis=-1),
# FIXME: Works currently for omega = zero, but for nonzero omega
# we need to integrate over theta at constant phi.
# Should be simple once we have coordinate mapping and source grid
# logic from GitHub pull request #1024.
line_label="theta",
fix_surface=("rho", max_rho),
expand_out=True,
)
# To approximate perimeter at ρ ~ 1, we scale by ρ⁻¹, assuming the integrand
# varies little from ρ = max_rho to ρ = 1.
# varies little from max ρ to ρ = 1.
/ max_rho
)
return data
Expand Down
18 changes: 14 additions & 4 deletions desc/equilibrium/equilibrium.py
Original file line number Diff line number Diff line change
Expand Up @@ -979,7 +979,6 @@ def need_src(name):
# Warn if best way to compute accurately is increasing resolution.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adjusted logic to be more self-consistent. Will raise all warnings raised previously and covers some additional cases.

for dep in deps:
req = data_index[p][dep]["resolution_requirement"]
coords = data_index[p][dep]["coordinates"]
msg = lambda direction: (
f"Dependency {dep} may require more {direction}"
" resolution to compute accurately."
Expand All @@ -988,23 +987,34 @@ def need_src(name):
# if need more radial resolution
"r" in req and grid.L < self.L_grid
# and won't override grid to one with more radial resolution
and not (override_grid and coords in {"z", ""}),
and not (
override_grid and (is_1dz_tor_grid(dep) or is_0d_vol_grid(dep))
),
ResolutionWarning,
msg("radial"),
)
warnif(
# if need more poloidal resolution
"t" in req and grid.M < self.M_grid
# and won't override grid to one with more poloidal resolution
and not (override_grid and coords in {"r", "z", ""}),
and not (
override_grid
and (
is_1dr_rad_grid(dep)
or is_1dz_tor_grid(dep)
or is_0d_vol_grid(dep)
)
),
ResolutionWarning,
msg("poloidal"),
)
warnif(
# if need more toroidal resolution
"z" in req and grid.N < self.N_grid
# and won't override grid to one with more toroidal resolution
and not (override_grid and coords in {"r", ""}),
and not (
override_grid and (is_1dr_rad_grid(dep) or is_0d_vol_grid(dep))
),
ResolutionWarning,
msg("toroidal"),
)
Expand Down
Loading
Loading