# -*- coding: utf-8 -*-
# Copyright (C) 2017,2018,2019,2020 Luiz Felippe S. Rodrigues <luizfelippesr@alumni.usp.br>
#
# This file is part of GalMag.
#
# GalMag is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# GalMag is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GalMag. If not, see <http://www.gnu.org/licenses/>.
#
"""
Contains the definition of the B_field and B_field_component classes.
"""
import numpy as np
from galmag.Grid import Grid
[docs]class B_field_component(object):
"""
A single galactic magnetic field component.
`B_field_component` objects store data for a single galactic magnetic
field component (e.g. the galactic *halo* magnetic field).
The properties x, y, z, r_spherical, r_cylindrical, theta and phi return
the magnetic field along each of these axis (with any required coordinate
transformation being performed on the fly).
Parameters
----------
grid : grid_object
See :class: `galmag.Grid`
x, y, z, r_spherical, r_cylindrical, theta, phi : 3D array_like
Values of the magnetic field in coordinates compatible with the grid
parameter should be supplied at initialization.
Unused coordinates should be set to None.
E.g. r_cylindrical=[[..],[..],[..]], z=[[..],[..],[..]],
phi=[[..],[..],[..]]
copy : bool
whether a fresh copy of the field data should be made upon
initialization. Default: True
"""
def __init__(self, grid, x=None, y=None, z=None, r_spherical=None,
r_cylindrical=None, theta=None, phi=None, copy=True,
dtype=np.dtype(np.float), generator=None, parameters={}):
assert(isinstance(grid, Grid))
self.grid = grid
self.dtype = np.dtype(dtype)
self.generator = generator
self.parameters = parameters
for component in ['x', 'y', 'z', 'r_spherical', 'r_cylindrical',
'theta', 'phi']:
data = eval(component)
setattr(self, '_'+component, None)
self.set_field_data(component, data, copy=copy)
@property
def x(self):
"""Horizontal coordinate component, :math:`x`"""
if self._x is None:
raiseQ = False
if (self._phi is not None and self._theta is not None):
# V_r*x/r - V_phi*sin(phi) + V_theta*cos(phi)*cos(theta)
if self._r_spherical is not None:
r_contribution = (self.r_spherical * self.grid.x /
self.grid.r_spherical)
elif self._r_cylindrical is not None:
r_contribution = (self.r_cylindrical * self.grid.x /
self.grid.r_cylindrical)
else:
raiseQ = True
self._x = (r_contribution -
self.phi * self.grid.sin_phi +
self.theta * self.grid.cos_phi *
self.grid.cos_theta)
elif (self._phi is not None and self._r_cylindrical is not None):
self._x = (self.r_cylindrical * self.grid.cos_phi
- self.phi * self.grid.sin_phi)
else:
raiseQ = True
if raiseQ:
raise ValueError(
"ERROR: x is neither directly nor indirectly defined.")
return self._x
@x.setter
def x(self, data):
self.set_field_data('x', data)
@property
def y(self):
"""Horizontal coordinate component, :math:`y`"""
if self._y is None:
raiseQ = False
if (self._phi is not None and self._theta is not None):
# V_r*y/r + V_phi*cos(phi) + V_theta*sin(phi)*cos(theta)
if self._r_spherical is not None:
r_contribution = (self.r_spherical * self.grid.y /
self.grid.r_spherical)
elif self._r_cylindrical is not None:
r_contribution = (self.r_cylindrical * self.grid.y /
self.grid.r_cylindrical)
else:
raiseQ = True
self._y = (r_contribution +
self.phi * self.grid.cos_phi +
self.theta * self.grid.sin_phi *
self.grid.cos_theta)
elif (self._phi is not None and self._r_cylindrical is not None):
self._y = (self.r_cylindrical * self.grid.sin_phi
+ self.phi * self.grid.cos_phi)
else:
raiseQ = True
if raiseQ:
raise ValueError(
"ERROR: y is neither directly nor indirectly defined.")
return self._y
@y.setter
def y(self, data):
self.set_field_data('y', data)
@property
def z(self):
"""Vertical coordinate component, :math:`z`"""
if self._z is None:
raiseQ = False
if self._theta is not None:
# V_r*z/r - V_theta*sin(theta)
if self._r_spherical is not None:
r_contribution = (self.r_spherical * self.grid.z /
self.grid.r_spherical)
elif self._r_cylindrical is not None:
r_contribution = (self.r_cylindrical * self.grid.z /
self.grid.r_cylindrical)
else:
raiseQ = True
self._z = (r_contribution - self.theta * self.grid.sin_theta)
else:
raiseQ = True
if raiseQ:
raise ValueError(
"ERROR: z is neither directly nor indirectly defined.")
return self._z
@z.setter
def z(self, data):
self.set_field_data('z', data)
@property
def r_spherical(self):
"""Spherical radial coordinate component, :math:`r`"""
if self._r_spherical is None:
# 1/r(V_x*x + V_y*y + V_z*z)
self._r_spherical = (self.x*self.grid.x +
self.y*self.grid.y +
self.z*self.grid.z)/self.grid.r_spherical
return self._r_spherical
@r_spherical.setter
def r_spherical(self, data):
self.set_field_data('r_spherical', data)
@property
def r_cylindrical(self):
"""Cylindrical radial coordinate component, :math:`s`"""
if self._r_cylindrical is None:
# 1/r(V_x*x + V_y*y)
self._r_cylindrical = ((self.x*self.grid.x +
self.y*self.grid.y) /
self.grid.r_cylindrical)
return self._r_cylindrical
@r_cylindrical.setter
def r_cylindrical(self, data):
self.set_field_data('r_cylindrical', data)
@property
def theta(self):
r"""Polar coordinate component, :math:`\theta`"""
if self._theta is None:
# V_x*cos(phi)*cos(theta) + V_y*sin(phi)*cos(theta) -
# V_z*sin(theta)
cos_theta = self.grid.cos_theta
self._theta = (self.x*self.grid.cos_phi*cos_theta +
self.y*self.grid.sin_phi*cos_theta -
self.z*self.grid.sin_theta)
return self._theta
@theta.setter
def theta(self, data):
self.set_field_data('theta', data)
@property
def phi(self):
r"""Azimuthal coordinate component, :math:`\phi`"""
if self._phi is None:
if (self._x is not None and self._y is not None):
# -V_x*sin(phi) + V_y*cos(phi)
self._phi = (-self.x*self.grid.sin_phi +
self.y*self.grid.cos_phi)
else:
raise ValueError(
"ERROR: phi is neither directly nor indirectly defined.")
return self._phi
@phi.setter
def phi(self, data):
self.set_field_data('phi', data)
[docs] def set_field_data(self, name, data, copy=True):
"""
Includes fresh data in a particular component
Parameters
----------
name : str
Name of the coordinate component
data : array_like
Data
copy : bool, optional
Copy (usual) or reference the data? Default: True.
"""
internal_field = getattr(self, "_"+name)
if (data is None) or (not copy):
setattr(self, "_"+name, data)
else:
setattr(self, "_"+name, data.copy())
[docs]class B_field(object):
"""
Galactic magnetic field
`B_field` objects store data for the whole galactic magnetic field.
Individual galactic magnetic field components (e.g. disk or halo) are
stored as attributes containing `B_field_component` objects.
The properties x, y, z, r_spherical, r_cylindrical, theta and phi return
the magnetic field along each of these axis. These are actually the sum of
all the galactic components available.
A disk or halo component can be added using the `add_disk_field` and
`add_halo_field` methods.
A custom new magnetic field component can be set using the
`set_field_component` method. Alternatively, the custom component can be
added during initialization as an extra keyword
(i.e. B_field.set_field_component(name, component) is equivalent to
B_field(..., name=component).
Parameters
----------
box : 3x2-array_like
Box limits
resolution : 3-array_like
containing the resolution along each axis.
grid_type : str, optional
Choice between 'cartesian', 'spherical' and 'cylindrical' *uniform*
coordinate grids. Default: 'cartesian'
dtype : numpy.dtype, optional
Data type used. Default: np.dtype(np.float)
"""
def __init__(self, box, resolution, grid_type='cartesian',
dtype=np.float, **kwargs):
self.dtype = dtype
self.box = np.empty((3, 2), dtype=self.dtype)
self.resolution = np.empty((3,), dtype=np.int)
# Uses numpy upcasting of scalars and dtype conversion
self.grid_type = grid_type
self.box[:] = box
self.resolution[:] = resolution
self.grid = Grid(box=self.box,
resolution=self.resolution,
grid_type=self.grid_type)
# If the user supplies pre-generated field components, add them
# to a list containing components names (for internal use)
self._components = []
self.parameters = {}
for key in kwargs:
assert(isinstance(kwargs[key], B_field_component))
self.set_field_component(key, kwargs[key])
# Includes the coordinate component attributes
for component in ['x', 'y', 'z', 'r_spherical', 'r_cylindrical',
'theta', 'phi']:
setattr(self, '_'+component, None)
@property
def x(self):
"""Horizontal coordinate component, :math:`x`"""
if self._x is None:
self._set_data('x')
return self._x
@property
def y(self):
"""Horizontal coordinate component, :math:`y`"""
if self._y is None:
self._set_data('y')
return self._y
@property
def z(self):
"""Vertical coordinate component, :math:`z`"""
if self._z is None:
self._set_data('z')
return self._z
@property
def r_spherical(self):
"""Spherical radial coordinate component, :math:`r`"""
if self._r_spherical is None:
self._set_data('r_spherical')
return self._r_spherical
@property
def r_cylindrical(self):
"""Cylindrical radial coordinate component, :math:`s`"""
if self._r_cylindrical is None:
self._set_data('r_cylindrical')
return self._r_cylindrical
@property
def theta(self):
r"""Polar coordinate component, :math:`\theta`"""
if self._theta is None:
self._set_data('theta')
return self._theta
@property
def phi(self):
r"""Azimuthal coordinate component, :math:`\phi`"""
if self._phi is None:
self._set_data('phi')
return self._phi
[docs] def add_disk_field(self, name='disk', **kwargs):
"""
Includes a disc magnetic field component.
Parameters
----------
name : str, optional
Name of the disc component. Default value: 'disk'
**kwargs :
See :class:`galmag.B_generator_disk` for the list of disc parameters
"""
import galmag.B_generators as Bgen
# First, prepares the generator
Bgen_disk = Bgen.B_generator_disk(grid=self.grid)
# Decides between find_B_field and get_B_field and computes
if 'reversals' in kwargs:
component = Bgen_disk.find_B_field(**kwargs)
elif 'disk_modes_normalization' in kwargs:
component = Bgen_disk.get_B_field(**kwargs)
else:
raise ValueError('Must specify either the positions of the reversals or the disk_modes_normalization.')
self.set_field_component(name, component)
[docs] def add_halo_field(self, name='halo', **kwargs):
"""
Parameters
----------
name :
(Default value = 'halo')
**kwargs :
Returns
-------
"""
import galmag.B_generators as Bgen
# First, prepares the generator
Bgen_halo = Bgen.B_generator_halo(grid=self.grid)
# Gets the field
component = Bgen_halo.get_B_field(**kwargs)
self.set_field_component(name, component)
[docs] def set_field_component(self, name, component):
"""
Parameters
----------
name :
component :
Returns
-------
"""
# Checks whether grid settings are compatible
if (np.any(component.grid.box != self.box) or
np.any(component.grid.resolution != self.resolution)):
raise ValueError('Incompatible grid geometry.')
# Adds the component (overwrites if it existent)
if name not in self._components:
self._components.append(name)
self.reset_cache()
setattr(self, name, component)
self.parameters.update(component.parameters)
[docs] def reset_cache(self):
"""
Erases cached total field values. Major components as 'disk' or 'halo'
are preserved and coordinate component values will be generated on next
time they are called.
"""
for component in ['x', 'y', 'z', 'r_spherical', 'r_cylindrical',
'theta', 'phi']:
setattr(self, '_'+component, None)
def _set_data(self, name):
"""
Updates the a coordinate component with the information in the various
field components (e.g. B.x = B.disk.x + B.halo.x)
Parameters
----------
name : str
Name of the coordinate component
"""
internal_field = None
for component in self._components:
component_field = getattr(self, component)
component_field_values = getattr(component_field, name)
if internal_field is None:
internal_field = component_field_values.copy()
else:
internal_field += component_field_values
setattr(self, '_'+name, internal_field)