#!/usr/bin/env python
r"""Special labels not handled by :py:class:`TeXlabel`."""
from pathlib import Path
from string import Template as StringTemplate
from string import Formatter as StringFormatter
from abc import abstractmethod
from . import base
class ArbitraryLabel(base.Base):
"""Abstract base class for custom labels."""
def __init__(self):
super().__init__()
@abstractmethod
def __str__(self):
pass
# @abstractproperty
# def tex(self):
# pass
# @abstractproperty
# def path(self):
# pass
class ManualLabel(ArbitraryLabel):
r"""Label defined by raw LaTeX text and unit."""
def __init__(self, tex, unit, path=None, description=None):
super().__init__()
self.set_tex(tex)
self.set_unit(unit)
self._path = path
self.set_description(description)
def __str__(self):
result = (
r"$\mathrm{%s} \; [%s]$"
% (
self.tex.replace(" ", r" \; "),
self.unit,
)
).replace(r"\; []", "")
return self._format_with_description(result)
@property
def tex(self):
return self._tex
@property
def unit(self):
return self._unit
@property
def path(self):
path = self._path
if path is None:
path = self.tex.replace(" ", "-")
path = Path(path)
return path
def set_tex(self, tex):
self._tex = tex.strip("$")
def set_unit(self, unit):
unit = base._inU.get(unit, unit)
self._unit = unit.strip("$")
[docs]
class Vsw(base.Base):
"""Solar wind speed."""
[docs]
def __init__(self, description=None):
super().__init__()
self.set_description(description)
# def __str__(self):
# return r"$%s \; [\mathrm{km \, s^{-1}}]$" % self.tex
@property
def tex(self):
return r"V_\mathrm{SW}"
@property
def units(self):
return r"\mathrm{km \, s^{-1}}"
@property
def path(self):
return Path("vsw")
class CarringtonRotation(ArbitraryLabel):
"""Carrington rotation count."""
def __init__(self, short_label=True, description=None):
"""Instantiate the label."""
super().__init__()
self._short_label = bool(short_label)
self.set_description(description)
def __str__(self):
result = r"$%s \; [\#]$" % self.tex
return self._format_with_description(result)
@property
def short_label(self):
return self._short_label
@property
def tex(self):
if self.short_label:
return r"\mathrm{CR}"
else:
return r"\mathrm{Carrington \; Rotation}"
@property
def path(self):
return Path("CarrRot")
[docs]
class Count(ArbitraryLabel):
"""Count histogram label."""
[docs]
def __init__(self, norm=None, description=None):
super().__init__()
self.set_axnorm(norm)
self.set_description(description)
self.build_label()
def __str__(self):
result = r"${} \; [{}]$".format(self.tex, self.units)
return self._format_with_description(result)
@property
def tex(self):
return self._tex
@property
def units(self):
return r"\#"
@property
def path(self):
return self._path
@property
def axnorm(self):
return self._axnorm
[docs]
def set_axnorm(self, norm):
if norm is not None:
norm = norm.lower()
assert norm in base._trans_axnorm.keys()
self._axnorm = norm
def _build_tex(self):
axnorm = self.axnorm
if axnorm:
if axnorm in ("r", "c", "t"):
tex = r"\mathrm{%s Norm Count}" % base._trans_axnorm.get(axnorm)
elif axnorm == "cd":
tex = r"\mathrm{1D Probability Density}"
elif axnorm == "rd":
tex = r"\mathrm{1D Probability Density}"
elif axnorm == "d":
tex = r"\mathrm{Probability Density}"
else:
raise ValueError(f"Unrecognized axis normalization `{axnorm}`")
else:
tex = r"\mathrm{Count}"
return tex.replace(" ", r" \, ")
def _build_path(self):
path = Path("count")
norm = self.axnorm
if norm is not None:
path = path / (norm.upper() + "norm")
return path
[docs]
def build_label(self):
self._tex = self._build_tex()
self._path = self._build_path()
class Power(ArbitraryLabel):
"""Power spectrum label."""
def __init__(self, description=None):
super().__init__()
self.set_description(description)
def __str__(self):
result = rf"${self.tex} \; [{self.units}]$"
return self._format_with_description(result)
@property
def tex(self):
return r"\mathrm{Power}"
@property
def units(self):
return base._inU["dimless"]
@property
def path(self):
return Path("power")
class Probability(ArbitraryLabel):
"""Probability that a quantity meets a comparison criterion."""
def __init__(self, other_label, comparison=None, description=None):
"""Instantiate the label."""
super().__init__()
self.set_other_label(other_label)
self.set_comparison(comparison)
self.set_description(description)
self.build_label()
def __str__(self):
result = r"${} \; [{}]$".format(self.tex, self.units)
return self._format_with_description(result)
@property
def tex(self):
return self._tex
@property
def units(self):
return r"\%"
@property
def path(self):
return self._path
@property
def other_label(self):
return self._other_label
@property
def comparison(self):
return self._comparison
def set_other_label(self, other):
assert isinstance(other, (str, base.Base))
self._other_label = other
def set_comparison(self, new):
if new is None:
new = ""
self._comparison = str(new)
def _build_tex(self):
other = self.other_label
tex = r"\mathrm{Prob.}(%s %s)" % (other.tex, self.comparison)
self._tex = tex.replace(" ", r" \, ")
def _build_path(self):
other = self.other_label
other = str(other.path)
other = other.replace(" ", "-")
comp = (
self.comparison.replace(">", "GT")
.replace("<", "LT")
.replace(r"\gt", "GT")
.replace(r"\lt", "GT")
.replace(r"\geq", "GEQ")
.replace(r"\leq", "LEQ")
.replace(r"\gt", "GT")
.replace(r"\neq", "NEQ")
.replace(r"\eq", "EQ")
.replace(r"==", "EQ")
.replace(r"!=", "NEQ")
.replace(r" ", "_")
)
path = Path(f"prob-{other}-{comp}")
self._path = path
def build_label(self):
self._build_tex()
self._build_path()
class CountOther(ArbitraryLabel):
"""Count of samples of another label fulfilling a comparison."""
def __init__(
self, other_label, comparison=None, new_line_for_units=False, description=None
):
"""Instantiate the label."""
super().__init__()
self.set_other_label(other_label)
self.set_comparison(comparison)
self.set_new_line_for_units(new_line_for_units)
self.set_description(description)
self.build_label()
def __str__(self):
result = r"${tex} {sep} [{units}]$".format(
tex=self.tex,
sep="$\n$" if self.new_line_for_units else r"\;",
units=self.units,
)
return self._format_with_description(result)
@property
def tex(self):
return self._tex
@property
def units(self):
return r"\#"
@property
def path(self):
return self._path
@property
def other_label(self):
return self._other_label
@property
def comparison(self):
return self._comparison
@property
def new_line_for_units(self):
return self._new_line_for_units
def set_new_line_for_units(self, new):
self._new_line_for_units = bool(new)
def set_other_label(self, other):
assert isinstance(other, (str, base.Base))
self._other_label = other
def set_comparison(self, new):
if new is None:
new = ""
self._comparison = str(new)
def _build_tex(self):
other = self.other_label
tex = r"\mathrm{Count.}(%s %s)" % (other.tex, self.comparison)
self._tex = tex.replace(" ", r" \, ")
def _build_path(self):
other = self.other_label
other = str(other.path)
other = other.replace(" ", "-")
comp = (
self.comparison.replace(">", "GT")
.replace("<", "LT")
.replace(r"\gt", "GT")
.replace(r"\lt", "GT")
.replace(r"\geq", "GEQ")
.replace(r"\leq", "LEQ")
.replace(r"\gt", "GT")
.replace(r"\neq", "NEQ")
.replace(r"\eq", "EQ")
.replace(r"==", "EQ")
.replace(r"!=", "NEQ")
)
path = Path(f"cnt-{other}-{comp}")
self._path = path
def build_label(self):
self._build_tex()
self._build_path()
class MathFcn(ArbitraryLabel):
"""Math function applied to another label."""
def __init__(
self,
fcn,
other_label,
dimensionless=True,
new_line_for_units=False,
description=None,
):
"""Instantiate the label."""
super().__init__()
self.set_other_label(other_label)
self.set_function(fcn)
self.set_dimensionless(dimensionless)
self.set_new_line_for_units(new_line_for_units)
self.set_description(description)
self.build_label()
def __str__(self):
sep = "$\n$" if self.new_line_for_units else r"\;"
result = rf"""${self.tex} {sep} \left[{self.units}\right]$"""
return self._format_with_description(result)
@property
def tex(self):
return self._tex
@property
def units(self):
if self.dimensionless:
return base._inU["dimless"]
return r"\mathrm{%s}\left(%s\right)" % (self.function, self.other_label.units)
@property
def path(self):
return self._path
@property
def other_label(self):
return self._other_label
@property
def function(self):
return self._function
@property
def dimensionless(self):
return self._dimensionless
@property
def new_line_for_units(self):
return self._new_line_for_units
def set_new_line_for_units(self, new):
self._new_line_for_units = bool(new)
def set_other_label(self, other):
assert isinstance(other, (str, base.Base))
self._other_label = other
def set_function(self, new):
if new is None:
new = ""
self._function = str(new)
def set_dimensionless(self, new):
self._dimensionless = bool(new)
def _build_tex(self):
# try:
# tex = other.tex
# except AttributeError:
tex = r"\mathrm{%s}(%s)" % (self.function, self.other_label.tex)
return tex.replace(" ", r" \, ")
def _build_path(self):
other = self.other_label
other = str(other.path)
# fcn = self.function
fcn = (
self.function.replace(r"\mathrm", "")
.replace("{", "")
.replace("}", "")
.replace("\\", "")
.replace(r"/", "-OV-")
)
path = Path(f"{fcn}-{other}")
return path
def build_label(self):
self._tex = self._build_tex()
self._path = self._build_path()
class AbsoluteValue(ArbitraryLabel):
"""Absolute value of another label, rendered as |...|.
Unlike MathFcn which can transform units (e.g., log makes things dimensionless),
absolute value preserves the original units since |x| has the same dimensions as x.
"""
def __init__(self, other_label, new_line_for_units=False, description=None):
"""Instantiate the label.
Parameters
----------
other_label : Base or str
The label to wrap with absolute value bars.
new_line_for_units : bool, default False
If True, place units on a new line.
description : str or None, optional
Human-readable description displayed above the mathematical label.
Notes
-----
Absolute value preserves units - |σc| has the same units as σc.
This differs from MathFcn(r"log_{10}", ..., dimensionless=True) where
the result is dimensionless.
"""
super().__init__()
self.set_other_label(other_label)
self.set_new_line_for_units(new_line_for_units)
self.set_description(description)
self.build_label()
def __str__(self):
sep = "$\n$" if self.new_line_for_units else r"\;"
result = rf"""${self.tex} {sep} \left[{self.units}\right]$"""
return self._format_with_description(result)
@property
def tex(self):
return self._tex
@property
def units(self):
"""Return units from underlying label - absolute value preserves dimensions."""
return self.other_label.units
@property
def path(self):
return self._path
@property
def other_label(self):
return self._other_label
@property
def new_line_for_units(self):
return self._new_line_for_units
def set_new_line_for_units(self, new):
self._new_line_for_units = bool(new)
def set_other_label(self, other):
assert isinstance(other, (str, base.Base))
self._other_label = other
def _build_tex(self):
return rf"\left|{self.other_label.tex}\right|"
def _build_path(self):
other = str(self.other_label.path)
return Path(f"abs-{other}")
def build_label(self):
self._tex = self._build_tex()
self._path = self._build_path()
class Distance2Sun(ArbitraryLabel):
"""Distance to the Sun."""
def __init__(self, units, description=None):
super().__init__()
self.set_units(units)
self.set_description(description)
def __str__(self):
result = r"$%s \; [\mathrm{%s}]$" % (self.tex, self.units)
return self._format_with_description(result)
@property
def units(self):
return self._units
@property
def path(self):
return Path("distance2sun")
@property
def tex(self):
return r"\mathrm{Distance \; to \; Sun}"
def set_units(self, units):
units = units.lower()
trans = {"rs": r"R_{\bigodot}", "re": r"R_{\oplus}", "au": r"\mathrm{AU}"}
units = trans.get(units, units)
if units not in [*trans.values()] + ["m", "km"]:
raise NotImplementedError("Unrecognized distance2sun units %s" % units)
self._units = units
class SSN(ArbitraryLabel):
"""Sunspot number label."""
def __init__(self, key, description=None):
super().__init__()
self.set_kind(key)
self.set_description(description)
def __str__(self):
result = r"$%s \; [\#]$" % self.tex
return self._format_with_description(result)
@property
def kind(self):
return self._kind
@property
def path(self):
return self._path
@property
def pretty_kind(self):
kind = self.kind
transform = {
"M": "Monthly",
"M13": "13 Month Smoothed",
"D": "Daily",
"Y": "Annual",
"NM": "Normalized Monthly",
"NM13": "Normalized 13 Month Smoothed",
"ND": "Normalized Daily",
"NY": "Normalized Yearly",
}
return transform[kind]
@property
def tex(self):
return (r"\mathrm{%s SSN}" % self.pretty_kind).replace(" ", r" \; ")
@property
def units(self):
return base._inU["dimless"]
def set_kind(self, new):
new = new.upper()
assert new in ("M", "M13", "D", "Y", "NM", "NM13", "ND", "NY")
self._kind = new
self._path = Path(f"""{new.upper()!s}ssn""")
class ComparisonLable(ArbitraryLabel):
"""Label comparing two other labels via a function."""
def __init__(self, labelA, labelB, fcn_name, fcn=None, description=None):
"""Instantiate the label."""
super().__init__()
self.set_constituents(labelA, labelB)
self.set_function(fcn_name, fcn)
self.set_description(description)
self.build_label()
def __str__(self):
result = r"${} \; [{}]$".format(self.tex, self.units)
return self._format_with_description(result)
@property
def tex(self):
return self._tex
@property
def units(self):
return self._units
@property
def path(self):
return self._path
@property
def labelA(self):
return self._labelA
@property
def labelB(self):
return self._labelB
@property
def function(self):
return self._function
@property
def function_name(self):
r"""Basically for use with building :py:meth:`path`."""
return self._function_name
def set_constituents(self, labelA, labelB):
if not isinstance(labelA, (str, base.Base)):
raise TypeError
if not isinstance(labelB, (str, base.Base)):
raise TypeError
if (
hasattr(labelA, "units")
and hasattr(labelB, "units")
and not (labelA.units == labelB.units)
):
raise ValueError(
rf"""If both {self.__class__.__name__} labels have units, they must be the same.
labelA : {labelA.units}
labelB : {labelB.units}
"""
)
elif hasattr(labelA, "units") and hasattr(labelB, "units"):
units = labelA.units
else:
units = "???"
self._labelA = labelA
self._labelB = labelB
self._units = units
def set_function(self, fcn_name, fcn):
if fcn is None:
get_fcn = fcn_name.lower()
translate = {
"subtract": r"{$labelA} - {$labelB}",
"add": r"{$labelA} + {$labelB}",
"multiply": r"{$labelA} \times {$labelB}",
}
fcn = translate.get(get_fcn)
keys = [x[1] for x in StringFormatter().parse(fcn)]
if not (("$labelA" in keys) and ("$labelB" in keys)):
raise ValueError(
rf"""{self.__class__.__name__}'s function must have the keys "$labelA" and "$labelB".
keys : {",".join(keys)}
"""
)
self._function = fcn
self._function_name = fcn_name
def _build_tex(self):
labelA = self.labelA
labelB = self.labelB
function = self.function
try:
texA = labelA.tex
except AttributeError:
texA = labelA
try:
texB = labelB.tex
except AttributeError:
texB = labelB
template = StringTemplate(function)
tex = template.safe_substitute(labelA=texA, labelB=texB)
while tex.find(r"\,\,") >= 0:
tex = tex.replace(r"\,\,", r"\,")
self._tex = tex
def _build_path(self):
labelA = self.labelA
labelB = self.labelB
try:
pathA = labelA.path
except AttributeError:
pathA = labelA
try:
pathB = labelB.path
except AttributeError:
pathB = labelB
pathA = str(pathA).replace(" ", "-")
pathB = str(pathB).replace(" ", "-")
function = self.function_name
path = Path(f"{function}-{pathA}-{pathB}")
self._path = path
def build_label(self):
self._build_tex()
self._build_path()
class Xcorr(ArbitraryLabel):
"""Cross-correlation coefficient between two labels."""
def __init__(self, labelA, labelB, method, short_tex=False, description=None):
"""Instantiate the label."""
super().__init__()
self.set_constituents(labelA, labelB)
self.set_method(method)
self.set_short_tex(short_tex)
self.set_description(description)
self.build_label()
def __str__(self):
result = r"${} \; [{}]$".format(self.tex, self.units)
return self._format_with_description(result)
@property
def tex(self):
return self._tex
@property
def units(self):
return r"\#"
@property
def short_tex(self):
return self._short_tex
@property
def path(self):
return self._path
@property
def labelA(self):
return self._labelA
@property
def labelB(self):
return self._labelB
@property
def method(self):
return self._method
def set_constituents(self, labelA, labelB):
if not isinstance(labelA, (str, base.Base)):
raise TypeError
if not isinstance(labelB, (str, base.Base)):
raise TypeError
self._labelA = labelA
self._labelB = labelB
def set_method(self, new):
self._method = str(new).title()
def set_short_tex(self, new):
self._short_tex = bool(new)
def _build_tex(self):
labelA = self.labelA
labelB = self.labelB
try:
texA = labelA.tex
except AttributeError:
texA = labelA
try:
texB = labelB.tex
except AttributeError:
texB = labelB
if self.short_tex:
tex = r"\rho_{%s}(%s,%s)" % (self.method[0], texA, texB)
else:
tex = r"\mathrm{{%s}}(%s,%s)" % (self.method, texA, texB)
tex = tex.replace(" ", r" \, ").replace(r" \, ", r"\,")
while tex.find(r"\,\,") >= 0:
tex = tex.replace(r"\,\,", r"\,")
self._tex = tex
def _build_path(self):
labelA = self.labelA
labelB = self.labelB
try:
pathA = labelA.path
except AttributeError:
pathA = labelA
try:
pathB = labelB.path
except AttributeError:
pathB = labelB
pathA = str(pathA).replace(" ", "-")
pathB = str(pathB).replace(" ", "-")
method = self.method
path = Path(f"Xcorr{method}-{pathA}-{pathB}")
self._path = path
def build_label(self):
self._build_tex()
self._build_path()