155 lines
5.7 KiB
Python
155 lines
5.7 KiB
Python
"""Represent models for near-Earth objects and their close approaches.
|
|
|
|
The `NearEarthObject` class represents a near-Earth object. Each has a unique
|
|
primary designation, an optional unique name, an optional diameter, and a flag
|
|
for whether the object is potentially hazardous.
|
|
|
|
The `CloseApproach` class represents a close approach to Earth by an NEO. Each
|
|
has an approach datetime, a nominal approach distance, and a relative approach
|
|
velocity.
|
|
|
|
A `NearEarthObject` maintains a collection of its close approaches, and a
|
|
`CloseApproach` maintains a reference to its NEO.
|
|
|
|
The functions that construct these objects use information extracted from the
|
|
data files from NASA, so these objects should be able to handle all of the
|
|
quirks of the data set, such as missing names and unknown diameters.
|
|
|
|
You'll edit this file in Task 1.
|
|
"""
|
|
|
|
from helpers import cd_to_datetime, datetime_to_str
|
|
import math
|
|
from datetime import datetime
|
|
|
|
|
|
class NearEarthObject:
|
|
"""A near-Earth object (NEO).
|
|
|
|
An NEO encapsulates semantic and physical parameters about the object, such
|
|
as its primary designation (required, unique), IAU name (optional), diameter
|
|
in kilometers (optional - sometimes unknown), and whether it's marked as
|
|
potentially hazardous to Earth.
|
|
|
|
A `NearEarthObject` also maintains a collection of its close approaches -
|
|
initialized to an empty collection, but eventually populated in the
|
|
`NEODatabase` constructor.
|
|
"""
|
|
|
|
def __init__(self, **info):
|
|
"""Create a new `NearEarthObject`.
|
|
|
|
:param info: A dictionary of excess keyword arguments supplied to the constructor.
|
|
"""
|
|
self.designation = info.get("designation")
|
|
self.name = None if info.get("name") == "" else info.get("name")
|
|
self.diameter = (
|
|
float("nan")
|
|
if info.get("diameter") in (None, "")
|
|
else float(info.get("diameter"))
|
|
)
|
|
self.hazardous = info.get("hazardous", "N") == "Y"
|
|
|
|
# Create an empty initial collection of linked approaches.
|
|
self.approaches = []
|
|
|
|
@property
|
|
def fullname(self):
|
|
"""Return a representation of the full name of this NEO."""
|
|
if self.name:
|
|
return f"{self.designation} {self.name}"
|
|
return f"{self.designation}"
|
|
|
|
def __str__(self):
|
|
"""Return `str(self)`."""
|
|
hazardous_str = (
|
|
"is potentially hazardous"
|
|
if self.hazardous
|
|
else "is not potentially hazardous"
|
|
)
|
|
if not math.isnan(self.diameter):
|
|
return f"NEO {self.fullname} has a diameter of {self.diameter:.3f} and {hazardous_str}."
|
|
return f"NEO {self.fullname} has an unknown diameter and {hazardous_str}."
|
|
|
|
def __repr__(self):
|
|
"""Return `repr(self)`, a computer-readable string representation of this object."""
|
|
return (
|
|
f"NearEarthObject(designation={self.designation!r}, name={self.name!r}, "
|
|
f"diameter={self.diameter:.3f}, hazardous={self.hazardous!r})"
|
|
)
|
|
|
|
def serialize(self):
|
|
"""Serialize this `NearEarthObject` into a dictionary."""
|
|
return {
|
|
"designation": self.designation,
|
|
"name": self.name,
|
|
"diameter_km": self.diameter,
|
|
"potentially_hazardous": self.hazardous,
|
|
}
|
|
|
|
|
|
class CloseApproach:
|
|
"""A close approach to Earth by an NEO.
|
|
|
|
A `CloseApproach` encapsulates information about the NEO's close approach to
|
|
Earth, such as the date and time (in UTC) of closest approach, the nominal
|
|
approach distance in astronomical units, and the relative approach velocity
|
|
in kilometers per second.
|
|
|
|
A `CloseApproach` also maintains a reference to its `NearEarthObject` -
|
|
initially, this information (the NEO's primary designation) is saved in a
|
|
private attribute, but the referenced NEO is eventually replaced in the
|
|
`NEODatabase` constructor.
|
|
"""
|
|
|
|
def __init__(self, **info):
|
|
"""Create a new `CloseApproach`.
|
|
|
|
:param info: A dictionary of excess keyword arguments supplied to the constructor.
|
|
"""
|
|
self._designation = info.get("designation")
|
|
|
|
self.time = info.get("time")
|
|
if self.time:
|
|
self.time = cd_to_datetime(info.get("time"))
|
|
|
|
self.distance = info.get("distance", float("nan"))
|
|
self.velocity = info.get("velocity", float("nan"))
|
|
|
|
# Create an attribute for the referenced NEO, originally None.
|
|
self.neo = None
|
|
|
|
@property
|
|
def time_str(self):
|
|
"""Return a formatted representation of this `CloseApproach`'s approach time.
|
|
|
|
The value in `self.time` should be a Python `datetime` object. While a
|
|
`datetime` object has a string representation, the default representation
|
|
includes seconds - significant figures that don't exist in our input
|
|
data set.
|
|
|
|
The `datetime_to_str` method converts a `datetime` object to a
|
|
formatted string that can be used in human-readable representations and
|
|
in serialization to CSV and JSON files.
|
|
"""
|
|
return f"{datetime_to_str(self.time)}"
|
|
|
|
def __str__(self):
|
|
"""Return `str(self)`."""
|
|
return f"On {self.time_str}, {self.neo.fullname} approaches Earth at a distance of {self.distance:.2f} au and a velocity of {self.velocity:.2f} km/s."
|
|
|
|
def __repr__(self):
|
|
"""Return `repr(self)`, a computer-readable string representation of this object."""
|
|
return (
|
|
f"CloseApproach(time={self.time_str!r}, distance={self.distance:.2f}, "
|
|
f"velocity={self.velocity:.2f}, neo={self.neo!r})"
|
|
)
|
|
|
|
def serialize(self):
|
|
"""Serialize this `CloseApproach` into a dictionary."""
|
|
return {
|
|
"datetime_utc": self.time_str,
|
|
"distance_au": self.distance,
|
|
"velocity_km_s": self.velocity,
|
|
}
|