Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions tests/test_formula_evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,53 @@ def evaluate_expr(source: str, columns: dict[str, Col]) -> object:
columns=_COLUMNS,
result_val=["NGC 123", "IC 456", "M 789"],
),
EvalCase(
name="to_deg_sexagesimal_hourangle",
expression='to_deg(col("ra"))',
columns={"ra": Col("00 02 08.4", "hourangle")},
result_val=0.535,
result_unit=u.deg,
),
EvalCase(
name="to_deg_sexagesimal_deg",
expression='to_deg(col("dec"))',
columns={"dec": Col("+16 35 13", "deg")},
result_val=16.5869,
result_unit=u.deg,
),
EvalCase(
name="to_deg_embedded_unit_string",
expression='to_deg(col("ra"))',
columns={"ra": Col("00h02m08.4s")},
result_val=0.535,
result_unit=u.deg,
),
EvalCase(
name="to_deg_quantity",
expression='to_deg(col("angle_col"))',
columns=_COLUMNS,
result_val=190.0,
result_unit=u.deg,
),
EvalCase(
name="to_deg_hourangle_quantity",
expression='to_deg(col("ra"))',
columns={"ra": Col(1.0, "hourangle")},
result_val=15.0,
result_unit=u.deg,
),
EvalCase(
name="string_column_with_unit_concat",
expression='col("ra") + "x"',
columns={"ra": Col("00 02 08.4", "hourangle")},
result_val="00 02 08.4x",
),
EvalCase(
name="error_to_deg_bare_string",
expression='to_deg(col("ra"))',
columns={"ra": Col("00 02 08.4")},
error=True,
),
]


Expand Down
1 change: 1 addition & 0 deletions tests/test_formula_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
('col("a") + col("b")', {"a", "b"}),
('col("weird name")', {"weird name"}),
('sin(col("pa")) + pi', {"pa"}),
('to_deg(col("RAJ2000"))', {"RAJ2000"}),
('3 * 10 ** col("logd25") * col("e_logd25") * arcsec', {"logd25", "e_logd25"}),
('"M " + col("id")', {"id"}),
("1 + 2", set()),
Expand Down
3 changes: 2 additions & 1 deletion uploader/app/lib/formula/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
ExpressionSyntaxError,
)
from uploader.app.lib.formula.namespace import expression_syntax_help
from uploader.app.lib.formula.values import Value, column_quantity
from uploader.app.lib.formula.values import TextValue, Value, column_quantity

__all__ = [
"Expression",
"ExpressionError",
"ExpressionEvaluationError",
"ExpressionSyntaxError",
"TextValue",
"Value",
"column_quantity",
"evaluate",
Expand Down
23 changes: 21 additions & 2 deletions uploader/app/lib/formula/namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import astropy.constants as const
import astropy.units as u
import numpy as np
from astropy.coordinates import Angle

from uploader.app.lib.formula.values import Value
from uploader.app.lib.formula.values import TextValue, Value

COL_FUNCTION = "col"

Expand All @@ -27,6 +28,8 @@ def _scalar_to_str(value: float | int | np.number) -> str:


def _formula_str(value: Value) -> str | np.ndarray:
if isinstance(value, TextValue):
return value.data
if isinstance(value, str):
return value
if isinstance(value, u.Quantity):
Expand All @@ -37,10 +40,24 @@ def _formula_str(value: Value) -> str | np.ndarray:
return np.asarray([_scalar_to_str(x) for x in value])


def _to_deg(value: object) -> u.Quantity:
if isinstance(value, TextValue):
angle = Angle(value.data, unit=u.Unit(value.unit)) if value.unit else Angle(value.data)
return angle.to(u.deg)
if isinstance(value, str):
return Angle(value).to(u.deg)
if isinstance(value, u.Quantity):
return value.to(u.deg)
if isinstance(value, np.ndarray):
return u.Quantity([_to_deg(item).value for item in value], unit=u.deg)
raise TypeError(f"to_deg() expected angle or coordinate string, got {type(value).__name__}")


FUNCTIONS: dict[str, object] = {
"sin": np.sin,
"cos": np.cos,
"str": _formula_str,
"to_deg": _to_deg,
}


Expand All @@ -63,7 +80,8 @@ def expression_syntax_help() -> str:

Mathematical operations:
- Operators: `+` `-` `*` `/` `**` `%`
- Functions: `sin(x)`, `cos(x)` (argument must be an angle), `str(x)`
- Functions: `sin(x)`, `cos(x)` (argument must be an angle), `str(x)`, `to_deg(x)`
- `to_deg(x)` parses coordinate strings or converts angle quantities to degrees
- Numbers are dimensionless
- String literals and `+` concatenation are supported
- Modulo divisors must carry units (e.g. `col("pa") % (180 * deg)`)
Expand All @@ -78,4 +96,5 @@ def expression_syntax_help() -> str:
- `180 * deg`
- `"G"` - fills the column with a text "G"
- Copy another column: `{COL_FUNCTION}("ra")`
- Sexagesimal coordinates: `to_deg({COL_FUNCTION}("RAJ2000"))`
- Mathematical expression: `3 * 10 ** {COL_FUNCTION}("logd25") * arcsec`"""
26 changes: 24 additions & 2 deletions uploader/app/lib/formula/values.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,32 @@
from collections.abc import Sequence
from dataclasses import dataclass
from typing import final

import astropy.units as u
import numpy as np
from astropy.units.function.core import FunctionUnitBase

type Value = u.Quantity | str | np.ndarray

@final
@dataclass(frozen=True)
class TextValue:
data: str
unit: str = ""

def __add__(self, other: object) -> str:
if isinstance(other, TextValue):
return self.data + other.data
if isinstance(other, str):
return self.data + other
return NotImplemented

def __radd__(self, other: object) -> str:
if isinstance(other, str):
return other + self.data
return NotImplemented


type Value = u.Quantity | str | TextValue | np.ndarray


def _is_logarithmic_column_unit(unit: u.Unit) -> bool:
Expand All @@ -13,7 +35,7 @@ def _is_logarithmic_column_unit(unit: u.Unit) -> bool:

def column_quantity(value: float | str | Sequence[float] | Sequence[str], unit: str) -> Value:
if isinstance(value, str):
return value
return TextValue(value, unit) if unit else value
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
if all(isinstance(x, str) for x in value):
return np.asarray(value)
Expand Down
3 changes: 3 additions & 0 deletions uploader/app/structured/designations/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from uploader.app.lib.formula import (
Expression,
ExpressionEvaluationError,
TextValue,
Value,
column_quantity,
evaluate,
Expand Down Expand Up @@ -139,6 +140,8 @@ def _build_column_values(
def _designation_string(value: Value) -> str:
if isinstance(value, str):
return value.strip()
if isinstance(value, TextValue):
return value.data.strip()
if isinstance(value, u.Quantity):
scalar = value.value
if isinstance(scalar, np.ndarray) and scalar.shape != ():
Expand Down
3 changes: 3 additions & 0 deletions uploader/app/structured/generic/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from uploader.app.lib.formula import (
Expression,
ExpressionEvaluationError,
TextValue,
Value,
column_quantity,
evaluate,
Expand Down Expand Up @@ -100,6 +101,8 @@ def _scalar_to_str(value: float | int | np.number) -> str:
def _value_to_str(value: Value) -> str:
if isinstance(value, str):
return value
if isinstance(value, TextValue):
return value.data
if isinstance(value, u.Quantity):
scalar = value.value
if isinstance(scalar, np.ndarray):
Expand Down
Loading