Source code for pyforestry.base.helpers.primitives.age
"""Primitive data types for representing stand age measurements."""
from enum import Enum
# --- Age Enum ---
[docs]
class Age(Enum):
"""
Enumeration of age measurement types.
Attributes:
TOTAL (Age): Total age measurement.
DBH (Age): Diameter at breast height age measurement.
"""
TOTAL = 1
DBH = 2
def __call__(self, value: float) -> "AgeMeasurement": # Use forward reference string
"""
Create an AgeMeasurement for this age type.
Args:
value (float): Measured age value; must be non-negative.
Returns:
AgeMeasurement: Instance with given value and this age code.
Raises:
ValueError: If value is negative.
"""
return AgeMeasurement(value, self.value)
# --- AgeMeasurement Class ---
[docs]
class AgeMeasurement(float):
"""
A float subclass representing an age measurement with type code.
Attributes:
code (int): Age enum code corresponding to measurement type.
"""
__slots__ = ("code",)
def __new__(cls, value: float, code: int):
"""
Create a new AgeMeasurement.
Args:
value (float): Age value; must be non-negative.
code (int): Numeric code from Age enum.
Returns:
AgeMeasurement: New instance with assigned code.
Raises:
ValueError: If value is negative or code invalid.
"""
if value < 0:
raise ValueError("Age must be non-negative.")
# Ensure code is valid using the Age enum definition
if code not in [m.value for m in Age]: # Check against Age enum values
allowed = [m.value for m in Age]
raise ValueError(f"Invalid age code: {code}. Must be one of {allowed}.")
obj = super().__new__(cls, value)
obj.code = code
return obj
def __getnewargs__(self) -> tuple:
"""Return the arguments ``__new__`` needs, so copying round-trips.
A ``float`` subclass is rebuilt by calling ``__new__`` with whatever
``__getnewargs__`` returns, and ``float``'s own returns the value alone.
Without this, ``copy.deepcopy`` of anything holding an age raised
``TypeError: AgeMeasurement.__new__() missing 1 required positional
argument: 'code'`` -- which is every :meth:`SimulationContext.checkpoint`
of a tree-list run, and every :func:`pyforestry.project`, since that
deep-copies the stand it is handed so the caller's inventory survives the
projection.
Returns:
The value and its age code, in ``__new__`` order.
"""
return (float(self), self.code)
@property
def value(self) -> float:
"""
Get the measurement value as a float.
Returns:
float: The age value.
"""
return float(self)
def __repr__(self):
"""
String representation including code and type name.
Returns:
str: Formatted repr string.
"""
# Ensure Age enum lookup is safe
age_type = "UNKNOWN"
try:
age_type = Age(self.code).name
except ValueError:
pass # Keep 'UNKNOWN' if code not in enum
return f"AgeMeasurement({float(self)}, code={self.code} [{age_type}])"
def __eq__(self, other):
"""
Equality comparison.
Compares both value and code for AgeMeasurement instances,
or value alone when comparing to a number.
Args:
other (AgeMeasurement|float|int): Object to compare.
Returns:
bool: True if equal.
"""
if isinstance(other, AgeMeasurement):
# *** Crucial: Compare both value and code ***
return float(self) == float(other) and self.code == other.code
elif isinstance(other, (float, int)):
# Comparison with plain number only checks value
return float(self) == float(other)
return NotImplemented
def __ne__(self, other):
"""Return the inverse result of :py:meth:`__eq__`."""
equal = self.__eq__(other)
return NotImplemented if equal is NotImplemented else not equal
# Defining __eq__ sets __hash__ to None (unhashable). Restore hashing on the
# float value so instances stay usable as dict keys / set members; this is
# consistent with __eq__ (equality requires equal float value, so equal
# objects hash equally — two same-value/different-code ages may share a hash,
# which is a permitted collision).
__hash__ = float.__hash__