Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

mgjeon
Copy link
Contributor

@mgjeon mgjeon commented Mar 9, 2025

Closes #194 by adding a new plotter CartesianPlotter for active region magnetic fields.

Description

  • This PR introduces CartesianPlotter, enabling visualization of active region magnetic fields in a local Cartesian coordinate system.
  • This implementation is a first step towards integrating RobertJaro/NF2 into sunkit-magex (see this issue).

Minimal Example

1. Analytical Magnetic Field

(Adapted from https://github.com/antyeates1983/flhtools)

Define magnetic field array and seed points

import numpy as np

def Bx(x, y, z):
    return x * 0 - 2

def By(x, y, z, t=2):
    return -z - t * (1 - z**2) / (1 + z**2 / 25)**2 / (1 + x**2 / 25)

def Bz(x, y, z):
    return y

nx, ny, nz = 64, 64, 64
x1 = np.linspace(-20, 20, nx)
y1 = np.linspace(-20, 20, ny)
z1 = np.linspace(0, 40, nz)
x, y, z = np.meshgrid(x1, y1, z1, indexing='ij')

bx, by, bz = Bx(x, y, z), By(x, y, z), Bz(x, y, z)
b = np.stack([bx, by, bz], axis=-1)
print(b.shape)  # (64, 64, 64, 3)

x_seed = np.linspace(-10, 10, 10)
y_seed = np.linspace(-10, 10, 5)
seeds = np.array([[x, y, 0] for x in x_seed for y in y_seed])
print(seeds.shape)  # (50, 3)

Plot

from sunkit_pyvista import CartesianPlotter

plotter = CartesianPlotter()
plotter.define_vector_field(b, grid_coords=(x1, y1, z1))
plotter.show_bounds()
plotter.plot_outline(color='black')
plotter.plot_boundary(show_scalar_bar=False)
plotter.plot_field_lines(seeds, color='blue', radius=0.1,
                         seeds_config=dict(show_seeds=True, color='red', point_size=10))
plotter.camera.azimuth = 210
plotter.camera.elevation = -5
plotter.camera.zoom(0.8)
plotter.show(jupyter_backend='static')

analytical

2. Numerical Magnetic Field

Download https://hinode.isee.nagoya-u.ac.jp/nlfff_database/v12/11890/20131106/11890_20131106_003600.nc

Define magnetic field array and seed points

import netCDF4

nc = netCDF4.Dataset('11890_20131106_003600.nc', 'r')
x1 = np.array(nc.variables['x'][:])
y1 = np.array(nc.variables['y'][:])
z1 = np.array(nc.variables['z'][:])
bx = np.array(nc.variables['Bx'][:].transpose(2, 1, 0))
by = np.array(nc.variables['By'][:].transpose(2, 1, 0))
bz = np.array(nc.variables['Bz'][:].transpose(2, 1, 0))
nc.close()

b = np.stack([bx, by, bz], axis=-1)
print(b.shape)  # (513, 257, 257, 3)

dx, dy = x1[1] - x1[0], y1[1] - y1[0]
x_seed = np.linspace(x1[0] + 100 * dx, x1[-1] - 100 * dx, 10)
y_seed = np.linspace(y1[0] + 50 * dy, y1[-1] - 50 * dy, 10)
seeds = np.array([[x, y, 0] for x in x_seed for y in y_seed])
print(seeds.shape)  # (100, 3)

Plot

from sunkit_pyvista import CartesianPlotter

plotter = CartesianPlotter()
plotter.define_vector_field(b, grid_coords=(x1, y1, z1))
plotter.show_bounds()
plotter.plot_outline(color='black')
plotter.plot_boundary(show_scalar_bar=True, clim=(-1000, 1000), 
                      scalar_bar_args=dict(position_x=0))
plotter.plot_field_lines(seeds, color='blue',
                         seeds_config=dict(show_seeds=False))
plotter.camera.azimuth = 210
plotter.camera.elevation = -5
plotter.camera.zoom(0.9)
plotter.show(jupyter_backend='static')

numerical

Feedback

You can test CartesianPlotter in this Colab notebook.

I'm not sure if this API design is appropriate. Also, I want to extend AR visualization to the spherical coordinate system, but I'm uncertain how to approach it. Please feel free to share your thoughts and suggestions.

@nabobalis
Copy link
Member

Thank you for this, I will try to review it in next two weeks.

@Cadair Cadair requested review from Cadair and nabobalis June 12, 2025 11:21
Copy link
Contributor

@dstansby dstansby left a comment

Choose a reason for hiding this comment

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

Looks good to me - I left a few suggestions

Comment on lines 623 to 626
self.grid = VectorGrid(vectors.astype(np.float64), *args, **kwargs)
self.xcoords = self.grid.xcoords.astype(np.float64)
self.ycoords = self.grid.ycoords.astype(np.float64)
self.zcoords = self.grid.zcoords.astype(np.float64)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is the casting to float64 needed here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

self.grid = VectorGrid(vectors.astype(np.float64), *args, **kwargs)

If the vectors of VectorGrid is not of type float64 (e.g., float32), it throws the following error:

Code

from streamtracer import StreamTracer, VectorGrid
grid = VectorGrid(vectors.astype(np.float32), grid_spacing=(1, 1, 1))
tracer = StreamTracer(10000, 0.1)
tracer.trace(seeds, grid)

Error

.../streamtracer/streamline.py:329, in StreamTracer.trace(self, seeds, grid, direction)
    ...
--> 329     xs_f, ns_f, ROT_f = trace_streamlines(
    ...
TypeError: argument 'values': 'ndarray' object cannot be converted to 'PyArray<T, D>'

Copy link
Contributor Author

Choose a reason for hiding this comment

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

self.xcoords = self.grid.xcoords.astype(np.float64)
self.ycoords = self.grid.ycoords.astype(np.float64)
self.zcoords = self.grid.zcoords.astype(np.float64)

For these three lines, I don't remember exactly, but I think I added them to avoid the following warning from pyvista.StructuredGrid:

Code

import pyvista as pv
from streamtracer import VectorGrid

grid = VectorGrid(vectors.astype(np.float64), grid_spacing=(1, 1, 1))
x, y, z = np.meshgrid(grid.xcoords, grid.ycoords, grid.zcoords, indexing="ij")
mesh = pv.StructuredGrid(x, y, z)

Warning

UserWarning: Points is not a float type. This can cause issues when transforming or applying filters. Casting to `np.float32`. Disable this by passing `force_float=False`.
  warnings.warn(

However, since PyVista internally casts to np.float32, casting the coordinates to np.float64 is unnecessary. Perhaps casting to np.float32, or just removing the casting and letting the warning be shown, could both be reasonable options. What do you think?

Copy link
Member

Choose a reason for hiding this comment

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

I don't like showing warnings but maybe its worth not casting and letting the warning show so users can do the casting themselves?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree. Showing the warning may help users better understand what's going on.

kwargs : dict
Keyword arguments for `pyvista.Plotter.add_mesh`.
"""
tracer = StreamTracer(max_steps, step_size)
Copy link
Member

Choose a reason for hiding this comment

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

VTK / pyvista has it's own streamline functionality which is probably a more natural fit for this package. Are you using streamtracer for some specific reason?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At first, I used PyVista's streamline function: https://github.com/mgjeon/magnetic_field_line/blob/main/magplot/base.py#L159

My goal was to trace field lines starting from the bottom, so I used the following code:

seed = pv.Plane(center=(self.grid.center[0], self.grid.center[1], 0), direction=(0,0,1), 
        i_size=i_size, j_size=j_size, 
        i_resolution=i_resolution, j_resolution=j_resolution)
strl = self.grid.streamlines_from_source(seed,
                                         vectors='vector',
                                         max_time=180,
                                         initial_step_length=0.1,
                                         integration_direction='both')

because I don't know how to provide seed points directly to PyVista's streamline API.

So I used StreamTracer instead, which accepts seed points directly via streamtracer.StreamTracer.trace(seeds, grid, direction=0).

Copy link
Member

Choose a reason for hiding this comment

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

I would be interested in the difference but if Streamtracer works, is it an issue if we added it as a dependency?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think people who use magnetic field line tracing may already use sunkit-magex, which has streamtracer as a dependency. So, adding it as a dependency is unlikely to be a major issue.

Copy link
Member

Choose a reason for hiding this comment

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

I don't think the dependency is a big issue, it's just the cost of bouncing around between VTK and back. I'm happy to merge this like this, maybe we could open an issue though?

Copy link
Member

Choose a reason for hiding this comment

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

Done

@nabobalis
Copy link
Member

Hi @mgjeon, sorry for the late reply.

Is it possible to add both examples as unit tests into the PR? we can create a figure test for them as well, so we can compare. Or even as an example?

What are your thoughts?

@mgjeon
Copy link
Contributor Author

mgjeon commented Aug 21, 2025

Hi @mgjeon, sorry for the late reply.

Is it possible to add both examples as unit tests into the PR? we can create a figure test for them as well, so we can compare. Or even as an example?

What are your thoughts?

I can add both to the sunkit-pyvista/examples folder as an example. But I’m not familiar with writing unit tests. Which code should I refer to?

@nabobalis
Copy link
Member

Hi @mgjeon, sorry for the late reply.
Is it possible to add both examples as unit tests into the PR? we can create a figure test for them as well, so we can compare. Or even as an example?
What are your thoughts?

I can add both to the sunkit-pyvista/examples folder as an example. But I’m not familiar with writing unit tests. Which code should I refer to?

Yeah, we can just start with the examples and then expand from there.

For the unit tests, I wonder if we can create something like this: https://github.com/sunpy/sunkit-pyvista/blob/main/sunkit_pyvista/tests/test_plotting.py#L31
but with your CartesianPlotter. We would need to swap out the input data with something and that might be the tricky part.

@mgjeon
Copy link
Contributor Author

mgjeon commented Sep 16, 2025

Hi @mgjeon, sorry for the late reply.
Is it possible to add both examples as unit tests into the PR? we can create a figure test for them as well, so we can compare. Or even as an example?
What are your thoughts?

I can add both to the sunkit-pyvista/examples folder as an example. But I’m not familiar with writing unit tests. Which code should I refer to?

Yeah, we can just start with the examples and then expand from there.

For the unit tests, I wonder if we can create something like this: https://github.com/sunpy/sunkit-pyvista/blob/main/sunkit_pyvista/tests/test_plotting.py#L31 but with your CartesianPlotter. We would need to swap out the input data with something and that might be the tricky part.

@nabobalis Sorry for the late reply. I added a figure test and example only for the analytical field case, since the numerical field (~1.5 GB) is too large and the usage is the same for both.

@nabobalis
Copy link
Member

Thank you! I rebased the PR with current origin and ran the pre commit.

It looks great to me.

@nabobalis nabobalis merged commit f2064d9 into sunpy:main Sep 19, 2025
8 of 11 checks passed
@nabobalis
Copy link
Member

I will fix the figure issues in a follow up PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add active region magnetic fields plotting
4 participants