def test_many_identifiers(identifier): """Common-case lookups of many SSO ids, passed as list, array, Series""" if isinstance(identifier, tuple): with pytest.raises(ValueError): rocks.identify(identifier) else: rocks.identify(identifier)
def rocks_(identifier, datacloud=[], progress=False): """Create multiple Rock instances. Parameters ========== identifier : list of str, list of int, list of float, np.array, pd.Series An iterable containing minor body identifiers. datacloud : list of str List of additional catalogues to retrieve from datacloud. Default is no additional catalogues. progress : bool Show progress of instantiation. Default is False. Returns ======= list of rocks.core.Rock A list of Rock instances """ if isinstance(identifier, pd.Series): identifier = identifier.values ids = [ id_ for _, _, id_ in rocks.identify( identifier, return_id=True, progress=progress) ] # type: ignore rocks_ = [ Rock(id_, skip_id_check=True, datacloud=datacloud) for id_ in tqdm( ids, desc="Building rocks : ", total=len(ids), disable=~progress) ] return rocks_
def id(id_): """Resolve the asteroid name and number from string input.""" name, number = rocks.identify(id_) # type: ignore if isinstance(name, (str)): click.echo(f"({number}) {name}") else: rocks.utils.list_candidate_ssos(id_)
def aliases(id_): """Echo the aliases of an asteroid.""" name, number, ssodnetid = rocks.identify(id_, return_id=True) # type: ignore aliases = rocks.index.get_aliases(ssodnetid) rich.print(f"({number}) {name}, aka \n {aliases}")
def info(id_): """Print the ssoCard of an asteroid.""" _, _, id_ = rocks.identify(id_, return_id=True) # type: ignore if not isinstance(id_, str): sys.exit() ssoCard = rocks.ssodnet.get_ssocard(id_) rich.print(ssoCard)
def confirm_identity(ids): """Confirm the SsODNet ID of the passed identifier. Retrieve the current ssoCard and remove the former one if the ID has changed. Parameters ---------- ids : list The list of SsODNet IDs to confirm. """ # Drop the named asteroids - their identity won't change ids = set(id_ for id_ in ids if not re.match(r"^[A-Za-z \(\)\_]*$", id_)) if not ids: return # nothing to do here elif len(ids) == 1: _, _, current_ids = rocks.identify(ids, return_id=True, local=False) current_ids = [current_ids] else: _, _, current_ids = zip( *rocks.identify(ids, return_id=True, local=False, progress=True)) # Swap the renamed ones updated = [] for old_id, current_id in zip(ids, current_ids): if old_id == current_id: continue rich.print( f"{old_id} has been renamed to {current_id}. Swapping the ssoCards." ) # Get new card and remove the old one rocks.ssodnet.get_ssocard(current_id, local=False) (rocks.PATH_CACHE / f"{old_id}.json").unlink() # This is now up-to-date updated.append(old_id) for id_ in updated: ids.remove(id_)
def identify(this): """Get asteroid name and number from string input. Parameters ========== this : str String to identify asteroid. """ name, number = rocks.identify(this) # type: ignore if isinstance(name, (str)): click.echo(f"({number}) {name}")
def info(this, minimal): """Print ssoCard of minor body. Parameters ========== this : str, optional Minor body name, designation, or number. If empty, a selection is prompted. """ if not this: _, _, this = rocks.utils.select_sso_from_index() else: # passed identified string, ensure that we know it _, _, this = rocks.identify(this, return_id=True) # type: ignore if not isinstance(this, str): sys.exit() ssoCard = rocks.ssodnet.get_ssocard(this) if ssoCard is not None: rocks.utils.pretty_print_card(ssoCard, minimal)
def create_index(): """Update index of numbered SSOs.""" # Get currently indexed objects index = read_index() # Get list of numbered asteroids from MPC url = "https://www.minorplanetcenter.net/iau/lists/NumberedMPs.txt" numbered = pd.read_fwf(url, colspecs=[(0, 7)], names=["number"]) numbered = set(int(n.strip(" (")) for n in numbered.number) # type: ignore # Compare list to index missing = set(index.number) ^ set(numbered) if not missing: return # Get ids of missing entries, append to index names, numbers, ids = zip(*rocks.identify(missing, return_id=True)) index = index.append( pd.DataFrame({ "name": names, "number": numbers, "id_": ids, })) # Save index to file index = (index.dropna(how="any").drop_duplicates("number").sort_values( "number", ascending=True).astype({ "number": np.int64, "name": str, "id_": str })) index = index.reset_index() with open(rocks.PATH_INDEX, "wb") as ind: pickle.dump(index, ind, protocol=4)
def test_query_and_resolve(id_, expected): """Test response of quaero queries. These can be local or remote queries.""" name, number = rocks.identify(id_) np.testing.assert_equal((name, number), expected)
# Download SDSS MOC1 (6.2MB) data = pd.read_fwf( "https://faculty.washington.edu/ivezic/sdssmoc/ADR1.dat.gz", colspecs=[(244, 250), (250, 270)], names=["numeration", "designation"], ) print(f"Number of observations in SDSS MOC1: {len(data)}") # Remove the unknown objects data = data[data.designation.str.strip(" ") != "-"] print(f"Observations of known objects: {len(set(data.designation))}") # ------ # Get current designations and numbers for objects # Unnumbered objects should be NaN data.loc[data.numeration == 0, "numeration"] = np.nan # Create list of identifiers by merging 'numeration' and 'designation' columns ids = data.numeration.fillna(data.designation) print("Identifying known objects in catalogue..") names_numbers = rocks.identify(ids) # Add numbers and names to data data["name"] = [name_number[0] for name_number in names_numbers] data["number"] = [name_number[1] for name_number in names_numbers] data.number = data.number.astype("Int64") # Int64 supports integers and NaN print(data.head())
def rocks_(ids, datacloud=None, progress=False, suppress_errors=False): """Create multiple Rock instances. Parameters ---------- ids : list of str, list of int, list of float, np.array, pd.Series An iterable containing minor body identifiers. datacloud : list of str List of additional catalogues to retrieve from datacloud. Default is no additional catalogues. progress : bool Show progress of instantiation. Default is False. suppress_errors: bool Do not print errors in the ssoCard. Default is False. Returns ------- list of rocks.core.Rock A list of Rock instances """ # Get IDs if len(ids) == 1 or isinstance(ids, str): ids = [rocks.identify(ids, return_id=True, progress=progress)[-1]] else: _, _, ids = zip(*rocks.identify(ids, return_id=True, progress=progress)) # Load ssoCards asynchronously rocks.ssodnet.get_ssocard( [id_ for id_ in ids if not id_ is None], progress=progress ) if datacloud is not None: if isinstance(datacloud, str): datacloud = [datacloud] # Load datacloud catalogues asynchronously for cat in datacloud: if cat not in rocks.datacloud.CATALOGUES.keys(): raise ValueError( f"Unknown datacloud catalogue name: '{cat}'" f"\nChoose from {rocks.datacloud.CATALOGUES.keys()}" ) rocks.ssodnet.get_datacloud_catalogue( [id_ for id_ in ids if not id_ is None], cat, progress=progress ) rocks_ = [ Rock( id_, skip_id_check=True, datacloud=datacloud, suppress_errors=suppress_errors, ) if not id_ is None else None for id_ in ids ] return rocks_
def __init__( self, id_, ssocard=None, datacloud=None, skip_id_check=False, suppress_errors=False, ): """Identify a minor body and retrieve its properties from SsODNet. Parameters ---------- id_ : str, int, float Identifying asteroid name, designation, or number ssocard : dict Optional argument providing a dictionary to use as ssoCard. Default is empty dictionary, triggering the query of an ssoCard. datacloud : list of str Optional list of additional catalogues to retrieve from datacloud. Default is no additional catalogues. skip_id_check : bool Optional argument to prevent resolution of ID before getting ssoCard. Default is False. suppress_errors: bool Do not print errors in the ssoCard. Default is False. Returns ------- rocks.core.Rock An asteroid class instance, with its properties as attributes. Notes ----- If the asteroid could not be identified or the data contains invalid types, the number is None and no further attributes but the name are set. Example ------- >>> from rocks import Rock >>> ceres = Rock('ceres') >>> ceres.taxonomy.class_ 'C' >>> ceres.taxonomy.shortbib 'DeMeo+2009' >>> ceres.diameter.value 848.4 >>> ceres.diameter.unit 'km' """ if isinstance(datacloud, str): datacloud = [datacloud] id_provided = id_ if not skip_id_check: _, _, id_ = rocks.identify(id_, return_id=True) # type: ignore # Get ssoCard and datcloud catalogues if not pd.isnull(id_): if ssocard is None: ssocard = rocks.ssodnet.get_ssocard(id_) if ssocard is None: # Asteroid does not have an ssoCard # Instantiate minimal ssoCard for meaningful error output. ssocard = {"name": id_provided} rich.print( f"Error 404: missing ssoCard for [green]{id_provided}[/green]." ) # This only gets printed once warnings.warn( "See https://rocks.readthedocs.io/en/latest/tutorials.html#error-404 for help." ) else: # rename 'spins' to 'spin' so it does not shadow the datacloud property # TODO this should be done at the parameter-name level if "spins" in ssocard["parameters"]["physical"]: ssocard["parameters"]["physical"]["spin"] = ssocard["parameters"][ "physical" ]["spins"] if datacloud is not None: for catalogue in datacloud: ssocard = self.__add_datacloud_catalogue( id_, catalogue, ssocard ) else: # Something failed. Instantiate minimal ssoCard for meaningful error output. ssocard = {"name": id_provided} # Deserialize the asteroid data try: super().__init__(**ssocard) # type: ignore except pydantic.ValidationError as message: if not suppress_errors: self.__parse_error_message(message, id_, ssocard) # Set the offending properties to None to allow for instantiation anyway for error in message.errors(): # Dynamically remove offending parts of the ssoCard offending_part = ssocard location_list = error["loc"][:-1] if any( property_ in location_list for property_ in ["taxonomy", "spin"] ): for property_ in ["taxonomy", "spin"]: if property_ in location_list: # these are lists instead of dicts and the indices are flipped # eg taxonomy bibref 0 becomes taxonomy 0 bibref idx = location_list.index(property_) entry, idx_list = location_list[idx + 1 : idx + 3] try: del ssocard["parameters"]["physical"][property_] except KeyError: pass else: for location in error["loc"][:-1]: offending_part = offending_part[location] del offending_part[error["loc"][-1]] super().__init__(**ssocard) # type: ignore # Convert the retrieve datacloud catalogues into DataCloudDataFrame objects if datacloud is not None: for catalogue in datacloud: if catalogue in ["diameters", "albedos"]: catalogue = "diamalbedo" try: catalogue_instance = rocks.datacloud.DataCloudDataFrame( data=getattr(self, catalogue).dict() ) # Common occurence of # ValueError: All arrays must be of the same length # due to malformed datacloud catalogue except ValueError: # Drop catalogue attributes with a single entry to_drop = [] for attribute, entries in getattr(self, catalogue).dict().items(): if len(entries) == 1: to_drop.append(attribute) for attribute in to_drop: delattr(getattr(self, catalogue), attribute) # Let's try this again catalogue_instance = rocks.datacloud.DataCloudDataFrame( data=getattr(self, catalogue).dict() ) # warnings.warn( # f"Removed malformed attributes {to_drop} from datacloud catalogue {catalogue}" # ) setattr(self, catalogue, catalogue_instance)
def __init__(self, id_, ssocard={}, datacloud=[], skip_id_check=False): """Identify a minor body and retrieve its properties from SsODNet. Parameters ========== id_ : str, int, float Identifying asteroid name, designation, or number ssocard : dict Optional argument providing a dictionary to use as ssoCard. Default is empty dictionary, triggering the query of an ssoCard. datacloud : list of str Optional list of additional catalogues to retrieve from datacloud. Default is no additional catalogues. skip_id_check : bool Optional argument to prevent resolution of ID before getting ssoCard. Default is False. Returns ======= rocks.core.Rock An asteroid class instance, with its properties as attributes. Notes ===== If the asteroid could not be identified or the data contains invalid types, the number is None and no further attributes but the name are set. Example ======= >>> from rocks import Rock >>> ceres = Rock('ceres') >>> ceres.taxonomy.class_ 'C' >>> ceres.taxonomy.shortbib 'DeMeo+2009' >>> ceres.diameter 848.4 >>> ceres.diameter.unit 'km' """ if isinstance(datacloud, str): datacloud = [datacloud] id_provided = id_ if not skip_id_check: _, _, id_ = rocks.identify(id_, return_id=True) # type: ignore # Get ssoCard and datcloud catalogues if not pd.isnull(id_): if not ssocard: ssocard = rocks.ssodnet.get_ssocard(id_) for catalogue in datacloud: ssocard = self.__add_datacloud_catalogue( id_, catalogue, ssocard) else: # Something failed. Instantiate minimal ssoCard for meaningful error output. ssocard = {"name": id_provided} # Deserialize the asteroid data try: super().__init__(**ssocard) # type: ignore except pydantic.ValidationError as message: self.__parse_error_message(message, id_, ssocard) return super().__init__(**{"name": id_provided})