-
Notifications
You must be signed in to change notification settings - Fork 47
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
Spec refactoring/improvement (PR 1 of 3 for single-stage) #418
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #418 +/- ##
==========================================
+ Coverage 92.00% 92.52% +0.52%
==========================================
Files 75 75
Lines 13504 13833 +329
==========================================
+ Hits 12424 12799 +375
+ Misses 1080 1034 -46
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
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.
Having the new Fourier transform and inverse transform functions go in the base Surface class feels awkward to me. These functions need mpol and ntor, which not all Surface classes have (e.g. SurfaceHenneberg, SurfaceGarabedian). Also the transform and inverse-transform functions don't use any of the surface geometry, just the grid points. So I would suggest moving those two functions to simsopt.util
(probably not in a class, just standalone functions).
It also feels awkward for NormalField to carry around computational_boundary
. See the comments attached to specific line numbers for some specific issues.
Codecov is flagging a lot of lines not covered by tests, 71% coverage for the changes compared to 91% for simsopt overall.
src/simsopt/field/normal_field.py
Outdated
|
||
The Fourier harmonics are the degees of freedom, the computational boundary is kept fixed. | ||
The normal field should not be normalized to unit area, i.e. it is the | ||
fourier components of B.(\grad\theta \times \nabla\zeta) on the surface. |
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.
But stepping back, why not choose the opposite convention, B dot n where n is the unit vector? Then computational_boundary.normal()
could be removed on line 487, making it mostly unnecessary for NormalField
to keep or refer to computational_boundary
. The only other dependence is that get_real_space_field()
uses quadrature points but those could be arguments.
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.
I use this convention because this is the way that SPEC requires the Fourier components of the normal field. This is currently the only use-case for NormalField, and it is simplest if the degrees-of-freedom correspond directly with the variables passed to SPEC.
It is possible to remove the convention and add have SPEC do the translation. or to pass to the class a convention='SPEC'
, which would raise a NotImplementedError
if another convention is requested. would that be a solution?
I do prefer to have the NormalField carry a reference to the surface on which it is calculated; without this the DoF's of NormalField are meaningless. If the user needs to keep track of which surface object the field is evaluated on, it makes sense to me to have NormalField to reference it. Modifying its DoFs I agree is not warranted.
The surface info is also used in helper functions such as get_real_space_field, and I would consider adding a .plot funcionality where it plots the field on the surface.
src/simsopt/geo/surface.py
Outdated
@@ -839,6 +839,130 @@ def interpolate_on_arclength_grid(self, function, theta_evaluate): | |||
|
|||
return function_interpolated | |||
|
|||
def fourier_transform_field(self, field, mpol=None, ntor=None, normalization=None, **kwargs): |
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.
Throughout this function and the inverse-transform function, the use of the word "field" is potentially confusing. It is natural to interpret it to mean the magnetic field, which is a vector. But I guess you mean any scalar field? It would be good to clarify this in the function names, docstrings, etc.
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.
I have changed the name to scalar
as it is a general method of transforming scalars, and adapted it in the docstrings. It is now also moved to surfaceRZFourier and not the Surface base class.
The calculation of such a transform always requires information from the surface on which the scalar quantity is evaluated. The function could be in util
, but I find it conceptually cleaner to have it as a method of the surfaceRZFourier.
tests/geo/test_surface_rzfourier.py
Outdated
|
||
# Test that all other elements are close to zero | ||
sines_mask = np.ones_like(ft_sines, dtype=bool) | ||
cosines_mask = sines_mask |
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.
You want a copy() here I think. Presently cosines_mask and sines_mask are two names for the same single object, so setting an entry in one modifies both.
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.
Thanks!
src/simsopt/geo/surface.py
Outdated
if not stellsym: | ||
cosangle = np.cos(angle) | ||
field = field + A_mns[m, n + ntor] * sinangle | ||
if not stellsym: | ||
field = field + A_mnc[m, n + ntor] * cosangle |
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.
The two if
statements can be combined into one.
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.
combined
I have adapted most changes and would like to move the review forward. Almost all new untested lines are now covered. Especially awaiting comments including a Adapted P.S. I might have accidentally 'started a review' myself, my apologies. |
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.
Hi @smiet, I see the testing was expanded but some of the issues above are still open. The main one is the location of the Fourier transform routines - I still think it doesn't make sense for them to be in the base Surface class, for the reasons explained above. There are also the issues about explaining that "field" is not a magnetic field vector, cosines_mask = sines_mask
, and combining if
statements
tests/geo/test_surface_rzfourier.py
Outdated
|
||
# create a test field where only Fourier elements [m=2, n=3] | ||
# and [m=4,n=5] are nonzero: | ||
field = field = 0.8 * np.sin(2*theta2d - 3*s.nfp*phi2d) + 0.2*np.sin(4*theta2d - 5*s.nfp*phi2d)+ 0.7*np.cos(3*theta2d - 3*s.nfp*phi2d) |
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.
Duplicate field =
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.
corrected
src/simsopt/field/normal_field.py
Outdated
vnc = np.zeros((self.mpol + 1, 2 * self.ntor + 1)) | ||
|
||
if surface is None: | ||
from simsopt.geo import SurfaceRZFourier |
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.
Why not put this import with the other imports at the top of the file?
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.
done
src/simsopt/field/normal_field.py
Outdated
@@ -91,6 +106,7 @@ def from_spec(cls, filename): | |||
if py_spec is None: | |||
raise RuntimeError( | |||
"Initialization from Spec requires py_spec to be installed.") | |||
from simsopt.geo import SurfaceRZFourier |
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.
Why not put this import with the others at the top of the file?
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.
done
src/simsopt/mhd/spec.py
Outdated
copy_to_pwd: boolean, if True, the default input file will be copied to the current working directory. Has to be set True as free-boundary SPEC can only handle files in the current working directory. | ||
verbose: boolean, if True, print statements will be printed | ||
""" | ||
import shutil, os |
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.
Imports usually go at the top of the file unless necessary
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.
moved
tests/mhd/test_spec.py
Outdated
test the default freeboundary file, and re-set if Picard | ||
iteration fails | ||
""" | ||
from simsopt._core.util import ObjectiveFailure |
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.
Imports to top of file
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.
done
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.
Hi Matt,
I am sorry, had entered many comments and discussion that are pending
(aparently not for a response from you, but not submitted and hence not visible to you!
My apologies, submitting these now and I will put the imports at the top of the file where requested.
src/simsopt/field/normal_field.py
Outdated
|
||
The Fourier harmonics are the degees of freedom, the computational boundary is kept fixed. | ||
The normal field should not be normalized to unit area, i.e. it is the | ||
fourier components of B.(\grad\theta \times \nabla\zeta) on the surface. |
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.
I use this convention because this is the way that SPEC requires the Fourier components of the normal field. This is currently the only use-case for NormalField, and it is simplest if the degrees-of-freedom correspond directly with the variables passed to SPEC.
It is possible to remove the convention and add have SPEC do the translation. or to pass to the class a convention='SPEC'
, which would raise a NotImplementedError
if another convention is requested. would that be a solution?
I do prefer to have the NormalField carry a reference to the surface on which it is calculated; without this the DoF's of NormalField are meaningless. If the user needs to keep track of which surface object the field is evaluated on, it makes sense to me to have NormalField to reference it. Modifying its DoFs I agree is not warranted.
The surface info is also used in helper functions such as get_real_space_field, and I would consider adding a .plot funcionality where it plots the field on the surface.
src/simsopt/geo/surface.py
Outdated
@@ -839,6 +839,130 @@ def interpolate_on_arclength_grid(self, function, theta_evaluate): | |||
|
|||
return function_interpolated | |||
|
|||
def fourier_transform_field(self, field, mpol=None, ntor=None, normalization=None, **kwargs): |
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.
I have changed the name to scalar
as it is a general method of transforming scalars, and adapted it in the docstrings. It is now also moved to surfaceRZFourier and not the Surface base class.
The calculation of such a transform always requires information from the surface on which the scalar quantity is evaluated. The function could be in util
, but I find it conceptually cleaner to have it as a method of the surfaceRZFourier.
src/simsopt/geo/surface.py
Outdated
if not stellsym: | ||
cosangle = np.cos(angle) | ||
field = field + A_mns[m, n + ntor] * sinangle | ||
if not stellsym: | ||
field = field + A_mnc[m, n + ntor] * cosangle |
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.
combined
tests/geo/test_surface_rzfourier.py
Outdated
|
||
# Test that all other elements are close to zero | ||
sines_mask = np.ones_like(ft_sines, dtype=bool) | ||
cosines_mask = sines_mask |
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.
Thanks!
… cbs/spec_update_rebase
I had forgotten to include the python 3.8 exception to the numpy 2.0 build-time dependency in the python wrapper. Just merged the commit into SPEC master right before you triggered the tests. Your commit just now should trigger the tests which will pull the update and they should now pass |
@landreman, @mbkumar, could this PR be moved forward? Because the discussion is mixed in with a lot of changed code, I am summarizing the main open threads: NormalField with a reference to the surface@landreman noted:
I believe this is necessary for the user, as just the Fourier components are meaningless without the surface. I have also added methods as NormalField.get_real_space_field() that performs the Fourier transform (using the surface properties) and is useful for plotting and analyzing the real space field (and passing to a Focus-like algorithm if you want to generate coils for your Spec). My insistence on this is also because of future development in PR3, where I will introduce a CoilNormalField that inherits from it, and requires the Surface. This way the two classes will have more attributes in common. Location of Fourier-transforming routine@landreman noted:
I agree that having the transform in the implementation of Fourier-transforming routine@andrewgiuliani noted:
I tried this, but failed, the current code works and is clear (though not optimally efficient), hope it is OK. |
Presently, the |
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.
Hi Chris, there are lines missing from code coverage and a merge conflict that needs to be resolved too.
I have covered the new and functionally significant added lines of code. The patch has increased coverage over the old state of the SPEC code (though still under target, sorry). Uncovered are mainly I hope to cover those in the future, but it will take time to generate a suitable nonstellaratorsymmetric spec file that runs in a reasonable time (for now we only test readin, the nonstellaratorsymmetric case takes cpu-hours to run and is unsitbable). With the increased coverage, passing tests, and removed duplicate transform method, I hope we are good to merge! Thank you all for your input and suggestions and time reviewing. |
Here's an example unit test for exceptions, it's not a massive change and it makes sure no lines of code in production are untested: simsopt/tests/geo/test_curve.py Line 166 in 2c0f057
|
I added the tests to the raises in NormalField. I will be doing some further additions during my next PR's is it OK if I catch the straggling uncovered exceptions in my next PR? |
Updates to Spec and NormalField and a few helper functions .
Spec class has been updated with the following features:
for vol in range(self.inputlist.mvol+1)
->for surface in self.initial_guess
)array_translator
to reduce looping over index counters.The NormalField class has been augmented to:
The Surface base class has two new methods:
fourier_transform_field
andinverse_fourier_transform_field
to efficiently calculate fourier transforms in the VMEC convention (-ntor
tontor
toroidal components,0
:mpol
poloidal). Allows for different Fourier normalization conventions through the 'normalization' parameter.This is PR 1 of 3, the following two: