Overview#
mapflpy centers around the Tracer and TracerMP classes
(both of which inherit from the base _Tracer class interface). The
Tracer class is for single-threaded tracing, while the TracerMP
class is for multi-processed tracing using python’s multiprocessing module.
The mapflpy_fortran shared-object is built from mapfl Fortran source code using
numpy.f2py and Meson. The Fortran routines are not
intended to be called directly by end-users, but rather through the Tracer and
TracerMP classes.
mapfl itself is not designed for object-oriented programming; rather, it is generally
called from the command line using a .in file to specify input arguments, and relies
on global variables to hold state information.
This package wraps the Fortran routines in a “pythonic” interface, allowing for a more flexible approach to tracing fieldlines. Until there is time (…) to refactor the Fortran code into a more modular design, these wrapper classes are the most feasible way to allow a broader audience to use the Fortran tracer.
Tracer vs. TracerMP#
The Tracer class directly imports the mapflpy_fortran cross-compiled Fortran
module. Since imports in Python are singletons, only one instance of the Fortran module can
exist within a process at a time.
As such, the Tracer class also enforces a singleton pattern (due to the fact that
the Fortran routines rely on global state variables, and multiple instances of the
Tracer class would lead to conflicts and race-conditions).
The TracerMP class, on the other hand, spawns multiple processes using the
multiprocessing module. Each instance of the TracerMP class creates
and links to a distinct subprocess, which imports its own instance of the Fortran module. A two-way pipe
is used to communicate between the main process and the subprocess. This allows multiple instances of the
Fortran routines to be used simultaneously, without conflicts (at the cost of inter-process communication
overhead). In an attempt to limit this overhead, magnetic field data is passed as filepaths.
These filepaths are passed to the respective subprocess over the pipe, then loaded into memory by the
subprocess itself.
Tracer as Dictionary#
The base _Tracer class implements the MutableMapping interface,
allowing for Tracer and TracerMP instances to behave like dictionaries. Under the hood,
the “mapfl.in” parameters are stored in a ChainMap which contains a set of default
parameters, as well as any user-specified changes (or additions) to the parameters.
1 from mapflpy.tracer import Tracer
2
3 tracer = Tracer()
4 current_params = dict(tracer) # get current parameters as a dictionary
5
6 tracer['verbose_'] = True # set verbose_ parameter to True
7 tracer.update(ds_min_=0.00001, ds_max_=10.1) # update multiple parameters at once
8 updated_params = dict(tracer) # get updated parameters as a dictionary
9
10 tracer.clear() # reset parameters to defaults
The one exception to this is the magnetic field data itself. Because this data is
handled differently in the Tracer and TracerMP
classes, the magnetic field data should be set using the br,
bt, and bp properties (or,
alternatively, the load_fields() method).
Warning
When using the TracerMP class, the magnetic field data
MUST be set using filepaths. Numpy arrays are NOT supported, e.g.
1from mapflpy.tracer import Tracer
2from psi_io import read_hdf_by_value
3
4# load magnetic field data from HDF files
5# read_hdf_by_value returns the data array followed by any scale arrays
6# e.g. values, r_scale, t_scale, p_scale
7br, *br_scales = read_hdf_by_value(ifile="br_file.h5")
8bt, *bt_scales = read_hdf_by_value(ifile="bt_file.h5")
9bp = "bp_file.h5"
10
11tracer = Tracer()
12tracer.br = br, *br_scales
13tracer.bt = bt, *bt_scales
14tracer.bp = bp # can be a filepath
or
1from mapflpy.tracer import TracerMP
2
3tracer_mp = TracerMP()
4tracer_mp.br = "br_file.h5"
5tracer_mp.bt = "bt_file.h5"
6tracer_mp.bp = "bp_file.h5"
Tracing Fieldines#
Once a Tracer or TracerMP
instance has been created, and the magnetic field data has been set, field lines
can be traced using the trace() method.
Note
Prior to tracing fieldlines the run() method
must be called viz. to populate any changes made to the input params or magnetic
field data.
With that said, the current “staleness” of the tracer instance can be checked using
the stale property i.e. whether there have been
any changes to the input parameters or magnetic field data since the last time the
last run() method was called.
1from mapflpy.tracer import Tracer
2
3tracer = Tracer()
4print(tracer.stale) # True, since nothing has been run yet
5tracer.run() # run the tracer to initialize
6print(tracer.stale) # False, since tracer is up-to-date
7tracer['ds_min_'] = 0.0001 # change a parameter
8print(tracer.stale) # True, since a parameter has changed
1from mapflpy.tracer import Tracer
2import numpy as np
3
4lps = [
5 np.full(10, 1.01), # r launch points [R_sun]
6 np.linspace(0, np.pi, 10), # theta launch points [rad]
7 np.zeros(10) # phi launch points [rad]
8]
9
10tracer = Tracer()
11tracer.load_fields( ... ) # load magnetic field data
12tracer.trace(lps, buffer_size=1000) # will call run() if stale
Traces can be performed “forward” or “backward” by calling the
set_tracing_direction() method with either
'f' (forward) or 'b' (backward).
Two separate calls must be made to trace in both directions. The resulting
trace geometry must then be combined manually. With that said, a utility function
combine_fwd_bwd_traces() is provided to help with this
common use case.
1from mapflpy.tracer import Tracer
2from mapflpy.utils import combine_fwd_bwd_traces
3
4tracer = Tracer()
5lps = [ ... ] # launch points
6tracer.load_fields( ... ) # load magnetic field data
7tracer.set_tracing_direction('f') # set to forward tracing
8fwd_traces = tracer.trace(lps)
9tracer.set_tracing_direction('b') # set to backward tracing
10bwd_traces = tracer.trace(lps)
11
12# combine the two traces into one, correctly ordered
13combined = combine_fwd_bwd_traces(fwd_traces, bwd_traces)
Example of iterating through a series of states within a time-dependent run:
1from mapflpy.tracer import Tracer
2import numpy as np
3
4traces =[]
5states = range(10, 20)
6
7lps = [
8 np.full(10, 1.01), # r launch points [R_sun]
9 np.linspace(0, np.pi, 10), # theta launch points [rad]
10 np.zeros(10) # phi launch points [rad]
11]
12
13tracer = Tracer()
14for state in states:
15 tracer.load_fields(
16 br=f"br_0000{state}.h5",
17 bt=f"bt_0000{state}.h5",
18 bp=f"bp_0000{state}.h5"
19 )
20 traces.append(tracer.trace(lps, buffer_size=1000)) # will call run() if stale
The result of trace() is a Traces object –
a named-tuple-like container for the traced fieldlines. This structure contains the following attributes:
geometry: an N x 3 x M array of the traced fieldline coordinates, i.e. the radial-theta-phi coordinates of the traces where N is the buffer size, and M is the number of fieldlines. (Note, to preserve a homogeneous array, field lines shorter than **N* are NaN padded).*start_pos: an M x 3 array of the starting positions of each fieldline.end_pos: an M x 3 array of the ending positions of each fieldline.traced_to_boundary: a boolean array of length M indicating whether each fieldline traced to a boundary (True) or was terminated early due to step-size constraints (False).
Using Scripts#
Several scripts are provided in the scripts module. These standalone functions
are used to perform common “one-off” tracing tasks (similar, in many respects, to how mapfl
itself is used from the command line).
Any additional keyword arguments (see function signature) provided within the calls
to these scripts are passed to the instantiation of the TracerMP class,
i.e. arguments used to set the mapfl parameters.
1from mapflpy.scripts import run_forward_tracing
2
3lps = [...]
4bfiles = {
5 'br': 'br_file.h5',
6 'bt': 'bt_file.h5',
7 'bp': 'bp_file.h5'
8}
9traces = run_forward_tracing(
10 **bfiles,
11 launch_points=lps, # <-- passed to trace() method
12 buffer_size=1000, # <-- passed to trace() method
13 domain_r_max_=100 # <-- example of passing mapfl parameter
14)
or
1from mapflpy.scripts import run_fwdbwd_tracing
2
3lps = [ ... ]
4bfiles = {
5 'br': 'br_file.h5',
6 'bt': 'bt_file.h5',
7 'bp': 'bp_file.h5'
8}
9traces = run_fwdbwd_tracing(
10 **bfiles,
11 launch_points=lps, # <-- passed to trace() method
12 buffer_size=1000, # <-- passed to trace() method
13)
This module also contains a script for performing interdomain tracing between
two different magnetic field domains (e.g. coronal to heliospheric). A more
comprehensive explanation of the function signature can be found in the documentation
for inter_domain_tracing().
A few general notes about this function:
requires 6 magnetic field files: 3 for the inner domain, and 3 for the outer domain.
the
r_interfaceparameter must be specified to indicate the radial location of the recross boundary between the two domains.the
helio_shiftparameter can be used to account for the longitudinal shift angle between the heliospheric domain and the coronal domain.
1from mapflpy.scripts import inter_domain_tracing
2from math import pi
3lps = [ ... ]
4coronal_bfiles = {
5 'br_cor': 'br_coronal.h5',
6 'bt_cor': 'bt_coronal.h5',
7 'bp_cor': 'bp_coronal.h5'
8}
9heliospheric_bfiles = {
10 'br_hel': 'br_helio.h5',
11 'bt_hel': 'bt_helio.h5',
12 'bp_hel': 'bp_helio.h5'
13}
14traces = inter_domain_tracing(
15 **coronal_bfiles,
16 **heliospheric_bfiles,
17 launch_points=lps,
18 r_interface=30.0, # <-- radial location of domain interface [R_sun]
19 helio_shift=pi/6, # <-- longitudinal shift between domains [rad]
20 rtol_=1e-6, # <-- relative tolerance used to determine when
21 # a fieldline has crossed the interface boundary
22)
Utilities#
A few utility functions are provided in the utils module.
get_fieldline_endpoints(), get_fieldline_npoints(),
and trim_fieldline_nan_buffer() can be used on raw fieldline geometry
(numpy arrays) or on Traces objects to extract information about the
traced field lines, or (in the case of trim_fieldline_nan_buffer())
to remove the NaN padding from field lines – returning a list of heterogeneously sized
field lines.
Lastly, get_fieldline_polarity() can be used to determine and classify
field lines as:
Enum Value |
Integer Value |
Description |
|---|---|---|
|
-2 |
Open field line with negative polarity at inner boundary |
|
-1 |
Closed field line |
|
0 |
Field line that failed to resolve within the buffer size |
|
1 |
Disconnected field line |
|
2 |
Open field line with positive polarity at inner boundary |
The result of this function is an array of type Polarity (an IntEnum class)
which provides a mapping between integer codes and string labels for each fieldline.