Update Exploring Near Earth Objects project and add Meme Generator project

This commit is contained in:
2026-01-03 21:55:24 -08:00
parent 9a4c3f7854
commit 155f0c9c6d
36 changed files with 754 additions and 65 deletions

View File

@@ -21,6 +21,7 @@ class NEODatabase:
help fetch NEOs by primary designation or by name and to help speed up
querying for close approaches that match criteria.
"""
def __init__(self, neos, approaches):
"""Create a new `NEODatabase`.
@@ -42,9 +43,16 @@ class NEODatabase:
self._neos = neos
self._approaches = approaches
# TODO: What additional auxiliary data structures will be useful?
self._neo_by_designation = {neo.designation: neo for neo in self._neos}
self._neo_by_name = {neo.name: neo for neo in self._neos}
# TODO: Link together the NEOs and their close approaches.
for approach in self._approaches:
# Link approach.neo to the corresponding NearEarthObject, namely
# the one whose designation matches approach._designation.
approach.neo = self._neo_by_designation[approach._designation]
# Add this approach to the corresponding NearEarthObject's
# .approaches collection.
self._neo_by_designation[approach._designation].approaches.append(approach)
def get_neo_by_designation(self, designation):
"""Find and return an NEO by its primary designation.
@@ -59,8 +67,7 @@ class NEODatabase:
:param designation: The primary designation of the NEO to search for.
:return: The `NearEarthObject` with the desired primary designation, or `None`.
"""
# TODO: Fetch an NEO by its primary designation.
return None
return self._neo_by_designation.get(designation, None)
def get_neo_by_name(self, name):
"""Find and return an NEO by its name.
@@ -76,8 +83,7 @@ class NEODatabase:
:param name: The name, as a string, of the NEO to search for.
:return: The `NearEarthObject` with the desired name, or `None`.
"""
# TODO: Fetch an NEO by its name.
return None
return self._neo_by_name.get(name, None)
def query(self, filters=()):
"""Query close approaches to generate those that match a collection of filters.
@@ -93,6 +99,7 @@ class NEODatabase:
:param filters: A collection of filters capturing user-specified criteria.
:return: A stream of matching `CloseApproach` objects.
"""
# TODO: Generate `CloseApproach` objects that match all of the filters.
for approach in self._approaches:
yield approach
cases = [filter_i(approach) for filter_i in filters]
if all(cases):
yield approach

View File

@@ -12,9 +12,9 @@ line, and uses the resulting collections to build an `NEODatabase`.
You'll edit this file in Task 2.
"""
import csv
import json
from models import NearEarthObject, CloseApproach
@@ -24,8 +24,16 @@ def load_neos(neo_csv_path):
:param neo_csv_path: A path to a CSV file containing data about near-Earth objects.
:return: A collection of `NearEarthObject`s.
"""
# TODO: Load NEO data from the given CSV file.
return ()
neos = []
with open(neo_csv_path, "r") as csv_file:
reader = csv.reader(csv_file)
next(reader)
for row in reader:
neo = NearEarthObject(
designation=row[3], name=row[4], diameter=row[15], hazardous=row[7]
)
neos.append(neo)
return neos
def load_approaches(cad_json_path):
@@ -34,5 +42,17 @@ def load_approaches(cad_json_path):
:param cad_json_path: A path to a JSON file containing data about close approaches.
:return: A collection of `CloseApproach`es.
"""
# TODO: Load close approach data from the given JSON file.
return ()
approaches = []
with open(cad_json_path, "r") as json_file:
cad = json.load(json_file)
for item in cad["data"]:
# 0: pdes, 3: cd, 4: dist, 7: v_rel
approach = CloseApproach(
designation=item[0],
time=item[3],
distance=float(item[4]),
velocity=float(item[7]),
)
approaches.append(approach)
return approaches

View File

@@ -16,7 +16,9 @@ iterator.
You'll edit this file in Tasks 3a and 3c.
"""
import operator
import itertools
class UnsupportedCriterionError(NotImplementedError):
@@ -38,6 +40,7 @@ class AttributeFilter:
Concrete subclasses can override the `get` classmethod to provide custom
behavior to fetch a desired attribute from the given `CloseApproach`.
"""
def __init__(self, op, value):
"""Construct a new `AttributeFilter` from an binary predicate and a reference value.
@@ -72,12 +75,57 @@ class AttributeFilter:
return f"{self.__class__.__name__}(op=operator.{self.op.__name__}, value={self.value})"
class DistanceFilter(AttributeFilter):
"""Subclass of AttributeFilter to filter by distance attribute."""
@classmethod
def get(cls, approach):
return approach.distance
class VelocityFilter(AttributeFilter):
"""Subclass of AttributeFilter to filter by velocity attribute."""
@classmethod
def get(cls, approach):
return approach.velocity
class DateFilter(AttributeFilter):
"""Subclass of AttributeFilter to filter by date attribute."""
@classmethod
def get(cls, approach):
return approach.time.date()
class DiameterFilter(AttributeFilter):
"""Subclass of AttributeFilter to filter by diameter attribute."""
@classmethod
def get(cls, approach):
return approach.neo.diameter
class HazardousFilter(AttributeFilter):
"""Subclass of AttributeFilter to filter by hazardous attribute."""
@classmethod
def get(cls, approach):
return approach.neo.hazardous
def create_filters(
date=None, start_date=None, end_date=None,
distance_min=None, distance_max=None,
velocity_min=None, velocity_max=None,
diameter_min=None, diameter_max=None,
hazardous=None
date=None,
start_date=None,
end_date=None,
distance_min=None,
distance_max=None,
velocity_min=None,
velocity_max=None,
diameter_min=None,
diameter_max=None,
hazardous=None,
):
"""Create a collection of filters from user-specified criteria.
@@ -108,8 +156,30 @@ def create_filters(
:param hazardous: Whether the NEO of a matching `CloseApproach` is potentially hazardous.
:return: A collection of filters for use with `query`.
"""
# TODO: Decide how you will represent your filters.
return ()
filters = []
if date:
filters.append(DateFilter(operator.eq, date))
if start_date:
filters.append(DateFilter(operator.ge, start_date))
if end_date:
filters.append(DateFilter(operator.le, end_date))
if distance_min:
filters.append(DistanceFilter(operator.ge, distance_min))
if distance_max:
filters.append(DistanceFilter(operator.le, distance_max))
if velocity_min:
filters.append(VelocityFilter(operator.ge, velocity_min))
if velocity_max:
filters.append(VelocityFilter(operator.le, velocity_max))
if diameter_min:
filters.append(DiameterFilter(operator.ge, diameter_min))
if diameter_max:
filters.append(DiameterFilter(operator.le, diameter_max))
if hazardous is not None:
filters.append(HazardousFilter(operator.eq, hazardous))
return filters
def limit(iterator, n=None):
@@ -121,5 +191,4 @@ def limit(iterator, n=None):
:param n: The maximum number of values to produce.
:yield: The first (at most) `n` values from the iterator.
"""
# TODO: Produce at most `n` values from the given iterator.
return iterator
return itertools.islice(iterator, n) if n else iterator

View File

@@ -17,7 +17,10 @@ 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:
@@ -32,22 +35,20 @@ class NearEarthObject:
initialized to an empty collection, but eventually populated in the
`NEODatabase` constructor.
"""
# TODO: How can you, and should you, change the arguments to this constructor?
# If you make changes, be sure to update the comments in this file.
def __init__(self, **info):
"""Create a new `NearEarthObject`.
:param info: A dictionary of excess keyword arguments supplied to the constructor.
"""
# TODO: Assign information from the arguments passed to the constructor
# onto attributes named `designation`, `name`, `diameter`, and `hazardous`.
# You should coerce these values to their appropriate data type and
# handle any edge cases, such as a empty name being represented by `None`
# and a missing diameter being represented by `float('nan')`.
self.designation = ''
self.name = None
self.diameter = float('nan')
self.hazardous = False
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 = []
@@ -55,20 +56,36 @@ class NearEarthObject:
@property
def fullname(self):
"""Return a representation of the full name of this NEO."""
# TODO: Use self.designation and self.name to build a fullname for this object.
return ''
if self.name:
return f"{self.designation} {self.name}"
return f"{self.designation}"
def __str__(self):
"""Return `str(self)`."""
# TODO: Use this object's attributes to return a human-readable string representation.
# The project instructions include one possibility. Peek at the __repr__
# method for examples of advanced string formatting.
return f"A NearEarthObject ..."
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})"
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:
@@ -84,21 +101,20 @@ class CloseApproach:
private attribute, but the referenced NEO is eventually replaced in the
`NEODatabase` constructor.
"""
# TODO: How can you, and should you, change the arguments to this constructor?
# If you make changes, be sure to update the comments in this file.
def __init__(self, **info):
"""Create a new `CloseApproach`.
:param info: A dictionary of excess keyword arguments supplied to the constructor.
"""
# TODO: Assign information from the arguments passed to the constructor
# onto attributes named `_designation`, `time`, `distance`, and `velocity`.
# You should coerce these values to their appropriate data type and handle any edge cases.
# The `cd_to_datetime` function will be useful.
self._designation = ''
self.time = None # TODO: Use the cd_to_datetime function for this attribute.
self.distance = 0.0
self.velocity = 0.0
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
@@ -116,19 +132,23 @@ class CloseApproach:
formatted string that can be used in human-readable representations and
in serialization to CSV and JSON files.
"""
# TODO: Use this object's `.time` attribute and the `datetime_to_str` function to
# build a formatted representation of the approach time.
# TODO: Use self.designation and self.name to build a fullname for this object.
return ''
return f"{datetime_to_str(self.time)}"
def __str__(self):
"""Return `str(self)`."""
# TODO: Use this object's attributes to return a human-readable string representation.
# The project instructions include one possibility. Peek at the __repr__
# method for examples of advanced string formatting.
return f"A CloseApproach ..."
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})"
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,
}

View File

@@ -10,6 +10,7 @@ extension determines which of these functions is used.
You'll edit this file in Part 4.
"""
import csv
import json
@@ -25,10 +26,24 @@ def write_to_csv(results, filename):
:param filename: A Path-like object pointing to where the data should be saved.
"""
fieldnames = (
'datetime_utc', 'distance_au', 'velocity_km_s',
'designation', 'name', 'diameter_km', 'potentially_hazardous'
"datetime_utc",
"distance_au",
"velocity_km_s",
"designation",
"name",
"diameter_km",
"potentially_hazardous",
)
# TODO: Write the results to a CSV file, following the specification in the instructions.
with open(filename, "w") as csv_file:
writer = csv.DictWriter(csv_file, fieldnames)
writer.writeheader()
for result in results:
row = {**result.serialize(), **result.neo.serialize()}
row["name"] = "" if row["name"] is None else row["name"]
row["diameter_km"] = (
"" if row["diameter_km"] is float("nan") else row["diameter_km"]
)
writer.writerow(row)
def write_to_json(results, filename):
@@ -42,4 +57,25 @@ def write_to_json(results, filename):
:param results: An iterable of `CloseApproach` objects.
:param filename: A Path-like object pointing to where the data should be saved.
"""
# TODO: Write the results to a JSON file, following the specification in the instructions.
data = []
for result in results:
item = {**result.serialize(), **result.neo.serialize()}
item["name"] = "" if item["name"] is None else item["name"]
item["diameter_km"] = (
"" if item["diameter_km"] is float("nan") else item["diameter_km"]
)
data.append(
{
"datetime_utc": item["datetime_utc"],
"distance_au": item["distance_au"],
"velocity_km_s": item["velocity_km_s"],
"neo": {
"designation": item["designation"],
"name": item["name"],
"diameter_km": item["diameter_km"],
"potentially_hazardous": item["potentially_hazardous"],
},
}
)
with open(filename, "w") as json_file:
json.dump(data, json_file, indent=2)