Skip to content

Merge pull request #3 from mvrl/copilot/update-readme-evaluation-scripts #14

Merge pull request #3 from mvrl/copilot/update-readme-evaluation-scripts

Merge pull request #3 from mvrl/copilot/update-readme-evaluation-scripts #14

Triggered via push April 27, 2026 21:58
Status Success
Total duration 19s
Artifacts

linting.yml

on: push
Run linters
15s
Run linters
Fit to window
Zoom out
Zoom in

Annotations

46 errors and 1 warning
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/check_wds.py#L1
import argparse import sys import tarfile import webdataset as wds from torch.utils.data import DataLoader + def log_and_continue(err): if isinstance(err, tarfile.ReadError) and len(err.args) == 3: print(err.args[2]) return True if isinstance(err, ValueError): return True raise err + def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) + if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( "--shardlist", required=True, help="Path to all shards in braceexpand format."
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/check_wds.py#L26
parser.add_argument("--batch-size", type=int, default=256, help="Batch size") parser.add_argument("--log-every", type=int, default=100, help="How often to log") args = parser.parse_args() keys = ("__key__", "jpg", "sci.txt", "com.txt", "sci_com.txt", "taxontag_com.txt") - + dataset = wds.DataPipeline( wds.SimpleShardList(args.shardlist), wds.tarfile_to_samples(handler=log_and_continue), wds.decode("torchrgb"), wds.to_tuple(*keys, handler=log_and_continue), ) - + dataloader = DataLoader( dataset, num_workers=args.workers, batch_size=args.batch_size ) itr = iter(dataloader)
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/check_wds.py#L49
batch_size = len(batch[0]) batches += 1 total_examples += batch_size if batches % args.log_every == 0: - eprint(f"{batches} batches / {total_examples} examples (current batch size: {batch_size})") + eprint( + f"{batches} batches / {total_examples} examples (current batch size: {batch_size})" + ) except StopIteration: break - eprint(f"Success! Processed {batches} batches with a total of {total_examples} examples.") + eprint( + f"Success! Processed {batches} batches with a total of {total_examples} examples." + )
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/check_taxa.py#L142
df - DataFrame with data source and common as columns. """ missing_common = df.loc[df.common.isna()] num_no_common = len(missing_common) if num_no_common > 0: - logging.warning(f"{num_no_common} entries have null common. They are from {missing_common.data_source.unique()}.") + logging.warning( + f"{num_no_common} entries have null common. They are from {missing_common.data_source.unique()}." + ) # print the first and last 5 entries with missing common if num_no_common > 0: logging.warning(f"First 5 entries with missing common: {missing_common.head()}") logging.warning(f"Last 5 entries with missing common: {missing_common.tail()}") - - def main(): # check for file if len(sys.argv) == 1:
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/check_taxa.py#L174
# If split column included, remove "train_small" entries as they are duplicates from "train". if "split" in list(df.columns): df = df.loc[df.split != "train_small"] - # Add data_source column to pin-point missing info + # Add data_source column to pin-point missing info df.loc[df["inat21_filename"].notna(), "data_source"] = "iNat21" df.loc[df["bioscan_filename"].notna(), "data_source"] = "BIOSCAN" df.loc[df["eol_content_id"].notna(), "data_source"] = "EOL" # Run checks
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/check_taxa.py#L187
check_hierarchy(df[TAXA]) check_sci_name(df[TAXA]) check_id(df[TAXA]) check_common(df[["data_source", "common"]]) + if __name__ == "__main__": main()
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/make_license_files.py#L1
""" Make license files for the various datasets that make up TreeOfLife-10M. """ + import argparse import csv import logging import pathlib import itertools
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/make_license_files.py#L66
if not tol10m_id: continue writer.writerow([tol10m_id, eol_content_id, eol_page_id, license, owner]) - if __name__ == "__main__": parser = init_parser() args = parser.parse_args() db = evobio10m.get_db(args.db)
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/make_mapping.py#L2
Reads all the files and makes a master mapping to specific files in EvoBio-10M. All files in the 10M have a UUID as the key. This script: 1. Creates the initial mapping (without reading the file contents) 2. Writes all these details to a sqlite database. """ + import argparse import logging import multiprocessing import os import tarfile
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/make_catalog_reproduce.py#L83
for shard in os.listdir(split_dir) if shard.endswith(".tar") ] if not shardlist: - logger.warn(f"No .tar files found in directory {split_dir}. Skipping this directory.") + logger.warn( + f"No .tar files found in directory {split_dir}. Skipping this directory." + ) return dataset = wds.DataPipeline( wds.SimpleShardList(shardlist), # Without this line, you will get num_workers copies
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/make_catalog_reproduce.py#L114
taxon, common_name, _ = taxon_data elif data_id.bioscan_filename: taxon_data = bioscan_name_lookup[key] logger.debug(f"bioscan_name_lookup[{key}] = {taxon_data}") if len(taxon_data) != 2: - logger.error(f"Unexpected value in bioscan_name_lookup: {taxon_data}") + logger.error( + f"Unexpected value in bioscan_name_lookup: {taxon_data}" + ) taxon, common_name, _ = taxon_data elif data_id.inat21_cls_name: taxon_data = inat21_name_lookup[data_id.inat21_cls_num] - logger.debug(f"inat21_name_lookup[{data_id.inat21_cls_num}] = {taxon_data}") + logger.debug( + f"inat21_name_lookup[{data_id.inat21_cls_num}] = {taxon_data}" + ) if len(taxon_data) != 2: - logger.error(f"Unexpected value in inat21_name_lookup: {taxon_data}") + logger.error( + f"Unexpected value in inat21_name_lookup: {taxon_data}" + ) taxon, common_name, _ = taxon_data else: raise ValueError(data_id) # Use get_common to generate the common name if not available
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/make_catalog_reproduce.py#L140
args = parser.parse_args() db = evobio10m.get_db(args.db) logger.info("Started.") - eol_name_lookup = naming_reproduce.load_name_lookup(disk_reproduce.eol_name_lookup_json, keytype=int) + eol_name_lookup = naming_reproduce.load_name_lookup( + disk_reproduce.eol_name_lookup_json, keytype=int + ) inat21_name_lookup = naming_reproduce.load_name_lookup( disk_reproduce.inat21_name_lookup_json, keytype=int ) - bioscan_name_lookup = naming_reproduce.load_name_lookup(disk_reproduce.bioscan_name_lookup_json) + bioscan_name_lookup = naming_reproduce.load_name_lookup( + disk_reproduce.bioscan_name_lookup_json + ) logger.info("Loaded name lookups.") key_lookup = {} # EOL stmt = "SELECT content_id, page_id, evobio10m_id FROM eol"
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/make_metadata.py#L1
""" Parses all metadata files into a single format that's easily read by make_wds.py. """ + import argparse import csv import json import logging import os
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/get_media_manifest.py#L7
import json from datetime import datetime from tempfile import TemporaryDirectory from tqdm import tqdm -''' +""" Usage: $ python get_media_manifest.py --output_dir /absolute/path/to/output_dir $ python get_media_manifest.py --output_dir ../relative/path/to/output_dir $ python get_media_manifest.py # saves the files in the current directory
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/get_media_manifest.py#L30
- The column names of the CSV file - The total number of entries in the combined CSV file The script will create the output directory if it does not exist upon confirmation by the user. If the output directory is not specified, the script will save the combined CSV file and log file in the current directory. -''' +""" DOWNLOAD_URL = "https://eol.org/data/media_manifest.tgz" def stream_download_file(file_path): - start_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") with requests.get(DOWNLOAD_URL, stream=True) as response: response.raise_for_status() - total_size_in_bytes = int(response.headers.get('content-length', 0)) - progress_bar = tqdm(total=total_size_in_bytes, unit='iB', unit_scale=True, desc="Downloading") + total_size_in_bytes = int(response.headers.get("content-length", 0)) + progress_bar = tqdm( + total=total_size_in_bytes, unit="iB", unit_scale=True, desc="Downloading" + ) with open(file_path, "wb") as file: for chunk in response.iter_content(chunk_size=8192): progress_bar.update(len(chunk)) file.write(chunk) progress_bar.close() - - md5_checksum = hashlib.md5(open(file_path, 'rb').read()).hexdigest() + + md5_checksum = hashlib.md5(open(file_path, "rb").read()).hexdigest() return start_time, md5_checksum + # Extract the numeric part from the chunked manifest file names def extract_numeric_part(file_name): - match = re.search(r'\d+', file_name) + match = re.search(r"\d+", file_name) if match: return int(match.group()) else: raise ValueError(f"No numeric part found in {file_name}") + def generate_output_filename(output_dir): - today = datetime.today().strftime('%Y-%m-%d') + today = datetime.today().strftime("%Y-%m-%d") filename = f"eol_media_manifest_{today}.csv" # If the file exists in this location, append a number to the filename i = 1 while os.path.exists(os.path.join(output_dir, filename)): filename = f"eol_media_manifest_{today}_{i}.csv" i += 1 return os.path.join(output_dir, filename) + def extract_and_combine_csv(tgz_file_path, output_file_path): - with tarfile.open(tgz_file_path, 'r:gz') as tar: - sorted_members = sorted(tar.getmembers(), key=lambda member: extract_numeric_part(member.name)) + with tarfile.open(tgz_file_path, "r:gz") as tar: + sorted_members = sorted( + tar.getmembers(), key=lambda member: extract_numeric_part(member.name) + ) - with open(output_file_path, 'w', encoding='utf-8') as output_file: + with open(output_file_path, "w", encoding="utf-8") as output_file: headers_written = False total_lines = 0 for member in tqdm(sorted_members, desc="Extracting files"): - if member.name.startswith("media_manifest_") and member.name.endswith(".csv"): + if member.name.startswith("media_manifest_") and member.name.endswith( + ".csv" + ): f = tar.extractfile(member) if f: - lines = f.read().decode('utf-8').splitlines() + lines = f.read().decode("utf-8").splitlines() if not headers_written: headers = lines[0] headers_written = True - output_file.write('\n'.join(lines)) - output_file.write('\n') + output_file.write("\n".join(lines)) + output_file.write("\n") total_lines += len(lines) - 1 return headers, total_lines -def create_json_log(log_filename, download_start_time, tgz_md5, csv_md5, output_file_path, headers, total_lines): + +def create_json_log( + log_filename, + download_start_time, + tgz_md5, + csv_md5, + output_file_path, + headers, + total_lines, +): log_data = { "download_start_time": download_start_time, "temp_tgz_md5": tgz_md5, "manifest_csv_md5": csv_md5, "manifest_csv_filepath": os.path.abspath(output_file_path), - "column_names": headers.split(','), - "total_entries_in_media_manifest": total_lines + "column_names": headers.split(","), + "total_entries_in_media_manifest": total_lines, } - with open(log_filename, 'w') as log_file: + with open(log_filename, "w") as log_file: json.dump(log_data, log_file, indent=4) + def main(output_dir): output_dir = os.path.abspath(output_dir) if not os.path.exists(output_dir): - confirm = input(f"The directory {output_dir} does not exist. Do you want it to be created now? (y/n): ") - if confirm.lower() != 'y': + confirm = input( + f"The directory {output_dir} does not exist. Do you want it to be created now? (y/n): " + ) + if confirm.lower() != "y": print("Directory creation cancelled. Exiting script.") return else: os.makedirs(output_dir) - + output_file_path = generate_output_filename(output_dir) base_output_file_name = os.path.splitext(output_file_path)[0] log_filename = f"{base_output_file_name}_log.json" - with TemporaryDirectory() as temp_dir: temp_file_path = os.path.join(temp_dir, "media_manifest.tgz") download_start_time, tgz_md5 = stream_download_file(temp_file_path) headers, total_lines = extract_and_combine_csv(temp_file_path, output_file_path) - csv_md5 = hashlib.md5(open(output_file_path, 'rb').read()).hexdigest() + csv_md5 = hashlib.md5(open(output_file_path, "rb").read()).hexdigest() - create_json_log(log_filename, download_start_time, tgz_md5, csv_md5, output_file_path, headers, total_lines) + create_json_log( + log_filename, + download_start_time, + tgz_md5, + csv_md5, + output_file_path, + headers, + total_lines, + ) print(f"EOL media manifest: {output_file_path}") print(f"Log file: {log_filename}") + if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Download, combine, and log EOL media manifest CSV files.") - parser.add_argument('--output_dir', type=str, default=".", - help="Directory to store the combined CSV file and log file. Defaults to the current directory.") + parser = argparse.ArgumentParser( + description="Download, combine, and log EOL media manifest CSV files." + ) + parser.add_argument( + "--output_dir", + type=str, + default=".", + help="Directory to store the combined CSV file and log file. Defaults to the current directory.", + ) args = parser.parse_args() main(args.output_dir) -
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/inat21_to_wds.py#L2
Converts iNat21 to a pretraining webdataset format. * Only includes 9K training classes so there are 1K unseen classes for evaluation * Uses both scientific and common names in the text descriptions """ + import os import torchvision import webdataset from tqdm.auto import tqdm
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/global_names_resolver.py#L1
import requests import json from tqdm import tqdm import argparse -''' + +""" Example usage: python global_names_resolver.py ../data/resolve-please.jsonl ../data/resolved.jsonl --batch_size 300 Note that performance seems to saturate beyond a batch size of 200-300 names.
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/global_names_resolver.py#L30
} } } The output contains the search term used, the name of the provider for the highest scoring result, and the 7-rank hierarchy. -''' +""" + # Function to resolve a batch of names and return structured data def resolve_names(names): - endpoint = 'http://resolver.globalnames.org/name_resolvers.json' - params = { - 'names': '|'.join(names), - 'with_canonical_ranks': 'true' - } + endpoint = "http://resolver.globalnames.org/name_resolvers.json" + params = {"names": "|".join(names), "with_canonical_ranks": "true"} response = requests.get(endpoint, params=params) if response.status_code != 200: return None data = response.json() structured_data_batch = {} - for item_data in data['data']: - if 'results' not in item_data: - continue + for item_data in data["data"]: + if "results" not in item_data: + continue highest_score = 0 highest_score_data = {} - for result in item_data['results']: - score = result.get('score', 0) - ranks = result.get('classification_path_ranks', '') - desired_ranks = ['kingdom', 'phylum', 'class', 'order', 'family', 'genus', 'species'] + for result in item_data["results"]: + score = result.get("score", 0) + ranks = result.get("classification_path_ranks", "") + desired_ranks = [ + "kingdom", + "phylum", + "class", + "order", + "family", + "genus", + "species", + ] if all(rank in ranks.lower() for rank in desired_ranks): - classification_path = result.get('classification_path', '') - data_source_title = result.get('data_source_title', '') - path_list = classification_path.split('|') - rank_list = ranks.lower().split('|') + classification_path = result.get("classification_path", "") + data_source_title = result.get("data_source_title", "") + path_list = classification_path.split("|") + rank_list = ranks.lower().split("|") if len(path_list) != len(rank_list): continue - rank_dict = {rank: term for rank, term in zip(rank_list, path_list) if rank in desired_ranks} + rank_dict = { + rank: term + for rank, term in zip(rank_list, path_list) + if rank in desired_ranks + } if score > highest_score: highest_score = score highest_score_data = {data_source_title: rank_dict} - name = item_data['supplied_name_string'] + name = item_data["supplied_name_string"] if highest_score > 0: structured_data_batch[name] = highest_score_data return structured_data_batch + def main(input_file_path, output_file_path, batch_size=300): # Get total number of lines in the input file for tqdm - with open(input_file_path, 'r') as infile: + with open(input_file_path, "r") as infile: total_lines = sum(1 for line in infile) # Read input file and loop through each line - with open(input_file_path, 'r') as infile: - with open(output_file_path, 'w') as outfile: + with open(input_file_path, "r") as infile: + with open(output_file_path, "w") as outfile: names_batch = [] for line in tqdm(infile, total=total_lines, unit="line"): entry = json.loads(line) name = f"{entry['genus']} {entry['species']}" names_batch.append(name) if len(names_batch) == batch_size: structured_data_batch = resolve_names(names_batch) if structured_data_batch: for name, data in structured_data_batch.items(): - outfile.write(json.dumps({name: data}) + '\n') + outfile.write(json.dumps({name: data}) + "\n") outfile.flush() names_batch = [] # Reset batch # Last batch if it has fewer names than the specified batch size if names_batch: structured_data_batch = resolve_names(names_batch) if structured_data_batch: for name, data in structured_data_batch.items(): - outfile.write(json.dumps({name: data}) + '\n') + outfile.write(json.dumps({name: data}) + "\n") outfile.flush() print("Data resolution complete.") + if __name__ == "__main__": - parser = argparse.ArgumentParser(description='Resolve species names to structured data') - parser.add_argument('input_file', help='Path to the input JSONL file') - parser.add_argument('output_file', help='Path to the output JSONL file') - parser.add_argument('--batch_size', type=int, default=300, help='Number of names to process in each batch') + parser = argparse.ArgumentParser( + description="Resolve species names to structured data" + ) + parser.add_argument("input_file", help="Path to the input JSONL file") + parser.add_argument("output_file", help="Path to the output JSONL file") + parser.add_argument( + "--batch_size", + type=int, + default=300, + help="Number of names to process in each batch", + ) args = parser.parse_args() - + main(args.input_file, args.output_file, args.batch_size) -
/home/runner/work/RCME/RCME/rcme/data/bioclip/src/imageomics/eol_reproduce.py#L1
import dataclasses import logging import re - logger = logging.getLogger() # ToL ID generated from uuid.uuid4(), uuid regex from https://stackoverflow.com/a/6640851 -tol_filename_pattern = re.compile("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}.*jpg$") +tol_filename_pattern = re.compile( + "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}.*jpg$" +) @dataclasses.dataclass(frozen=True) class ImageFilename: """
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/make_wds.py#L1
""" Writes the training and validation data to webdataset format. """ + import argparse import collections import json import logging import multiprocessing
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/make_wds_reproduce.py#L1
""" Writes the training and validation data to webdataset format. """ + import argparse import collections import json import logging import multiprocessing import os import tarfile from PIL import Image, ImageFile -from imageomics import disk_reproduce, eol_reproduce, evobio10m_reproduce, naming_reproduce, wds +from imageomics import ( + disk_reproduce, + eol_reproduce, + evobio10m_reproduce, + naming_reproduce, + wds, +) ######## # CONFIG ########
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/make_wds_reproduce.py#L67
evobio10m_id: (content_id, page_id) for evobio10m_id, content_id, page_id in db.execute(select_stmt).fetchall() } db.close() - name_lookup = naming_reproduce.load_name_lookup(disk_reproduce.eol_name_lookup_json, keytype=int) + name_lookup = naming_reproduce.load_name_lookup( + disk_reproduce.eol_name_lookup_json, keytype=int + ) # r|gz indcates reading from a gzipped file, streaming only with tarfile.open(imgset_path, "r|gz") as tar: for i, member in enumerate(tar): eol_img = eol_reproduce.ImageFilename.from_filename(member.name) if eol_img.raw in image_blacklist: continue - + # Match on treeoflife_id filename if eol_img.tol_id not in eol_ids_lookup: print(f"Can't find the tol_id {eol_img.tol_id}") logger.warning( "EvoBio10m ID missing. [tol_id: %s]",
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/make_wds_reproduce.py#L88
continue # fetching page ID content_id, page_id = eol_ids_lookup[eol_img.tol_id] global_id = eol_img.tol_id - + # checking for global id in split if global_id not in splits[args.split] or global_id in finished_ids: continue - + # checking for page id if page_id not in name_lookup: continue - + # using name lookup for taxon, common, classes taxon, common, classes = name_lookup[page_id] if taxon.scientific in species_blacklist: continue
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/make_wds_reproduce.py#L112
logger.warning( "Error opening file. Skipping. [tar: %s, err: %s]", imgset_path, err ) continue - txt_dct = make_txt(taxon, common) + txt_dct = make_txt(taxon, common) # writing EOL sink.write( - {"__key__": global_id, "jpg": img, **txt_dct} #, "classes": classes} # REMOVED "classes", unused list of numbers, throws an error as an unknown extension with webdataset + { + "__key__": global_id, + "jpg": img, + **txt_dct, + } # , "classes": classes} # REMOVED "classes", unused list of numbers, throws an error as an unknown extension with webdataset ) ######## # INAT21
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/make_wds_reproduce.py#L135
(filename, cls_num): evobio10m_id for evobio10m_id, filename, cls_num in db.execute(select_stmt).fetchall() } db.close() - name_lookup = naming_reproduce.load_name_lookup(disk_reproduce.inat21_name_lookup_json, keytype=int) + name_lookup = naming_reproduce.load_name_lookup( + disk_reproduce.inat21_name_lookup_json, keytype=int + ) clsdir_path = os.path.join(disk_reproduce.inat21_root_dir, clsdir) for i, filename in enumerate(os.listdir(clsdir_path)): filepath = os.path.join(clsdir_path, filename)
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/make_wds_reproduce.py#L159
taxon, common, classes = name_lookup[cls_num] if taxon.scientific in species_blacklist: continue - txt_dct = make_txt(taxon, common) #, classes) + txt_dct = make_txt(taxon, common) # , classes) img = load_img(filepath).resize(resize_size) # writing iNat - sink.write({"__key__": global_id, "jpg": img, **txt_dct} #, "classes": classes} # REMOVED "classes", unused list of numbers, throws an error as an unknown extension with webdataset - ) + sink.write( + { + "__key__": global_id, + "jpg": img, + **txt_dct, + } # , "classes": classes} # REMOVED "classes", unused list of numbers, throws an error as an unknown extension with webdataset + ) ######### # BIOSCAN #########
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/make_wds_reproduce.py#L182
(part, filename): evobio10m_id for evobio10m_id, part, filename in db.execute(select_stmt).fetchall() } db.close() - name_lookup = naming_reproduce.load_name_lookup(disk_reproduce.bioscan_name_lookup_json) + name_lookup = naming_reproduce.load_name_lookup( + disk_reproduce.bioscan_name_lookup_json + ) partdir = os.path.join(disk_reproduce.bioscan_root_dir, f"part{part}") for i, filename in enumerate(os.listdir(partdir)): if (part, filename) not in evobio10m_id_lookup: logger.warning(
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/make_wds_reproduce.py#L201
taxon, common, classes = name_lookup[global_id] if taxon.scientific in species_blacklist: continue - txt_dct = make_txt(taxon, common) #, classes) + txt_dct = make_txt(taxon, common) # , classes) filepath = os.path.join(partdir, filename) img = load_img(filepath).resize(resize_size) # writing BIOSCAN - sink.write({"__key__": global_id, "jpg": img, **txt_dct} #, "classes": classes} # REMOVED "classes", unused list of numbers, throws an error as an unknown extension with webdataset - ) + sink.write( + { + "__key__": global_id, + "jpg": img, + **txt_dct, + } # , "classes": classes} # REMOVED "classes", unused list of numbers, throws an error as an unknown extension with webdataset + ) ###### # MAIN ######
/home/runner/work/RCME/RCME/rcme/data/bioclip/scripts/evobio10m/make_wds_reproduce.py#L265
assert taxon is not None assert not taxon.empty, f"{common} has no taxon!" # test replacing the two if statements with common = naming_reproduce.get_common(taxon, common) - '''if not common: + """if not common: common = taxon.scientific if not common: - common = taxon.taxonomic''' + common = taxon.taxonomic""" # ex: kingdom Animalia phylum Arthropoda class ... tagged = " ".join(f"{tier} {value}" for tier, value in taxon.tagged) # IF YOU UPDATE THE KEYS HERE, BE SURE TO UPDATE check_status() TO LOOK FOR ALL OF
/home/runner/work/RCME/RCME/rcme/data/bioclip/src/imageomics/evobio10m_reproduce.py#L1
import dataclasses import sqlite3 import os base_dir = os.path.dirname(os.path.abspath(__file__)) + def get_outdir(tag): outdir = "data/TreeOfLife-10M/dataset/evobio10m" return os.path.abspath(f"{outdir}-{tag}") # return f"/fs/ess/PAS2136/open_clip/data/evobio10m-{tag}"
/home/runner/work/RCME/RCME/rcme/data/bioclip/src/imageomics/naming_eval.py#L9
from . import disk, helpers logger = logging.getLogger() + def load_json(filepath): - with open(filepath, 'r') as f: + with open(filepath, "r") as f: return json.load(f) + def dataset_class_to_taxon(cls): tiers = cls.split("_") if cls[0].isdigit(): index, *tiers = cls.split("_") index = int(index, base=10) return Taxon(*tiers, dataset_id=index) else: return Taxon(*cls.split("_")) + @dataclasses.dataclass class Taxon: kingdom: str = "" phylum: str = ""
/home/runner/work/RCME/RCME/rcme/data/bioclip/src/imageomics/naming_eval.py#L60
"order": self.order.capitalize(), "family": self.family.capitalize(), "genus": self.genus.capitalize(), "species": self.species.lower(), } - + @Property def scientific_name(self): - if not self.subspecies == '': - return self.genus.capitalize() + ' ' + self.species.lower() + ' ' + self.subspecies.lower() - elif not self.species == '': - return self.genus.capitalize() + ' ' + self.species.lower() - elif not self.genus == '': + if not self.subspecies == "": + return ( + self.genus.capitalize() + + " " + + self.species.lower() + + " " + + self.subspecies.lower() + ) + elif not self.species == "": + return self.genus.capitalize() + " " + self.species.lower() + elif not self.genus == "": return self.genus.capitalize() else: return None - + @Property def taxonomic_name(self): - if not self.subspecies == '': + if not self.subspecies == "": if self.kingdom == "": - return " ".join([ - self.genus.capitalize(), - self.species.lower(), - self.subspecies.lower() - ]) + return " ".join( + [ + self.genus.capitalize(), + self.species.lower(), + self.subspecies.lower(), + ] + ) else: - return " ".join([ - self.kingdom.capitalize(), - self.phylum.capitalize(), - self.cls.capitalize(), - self.order.capitalize(), - self.family.capitalize(), - self.genus.capitalize(), - self.species.lower(), - self.subspecies.lower() - ]) + return " ".join( + [ + self.kingdom.capitalize(), + self.phylum.capitalize(), + self.cls.capitalize(), + self.order.capitalize(), + self.family.capitalize(), + self.genus.capitalize(), + self.species.lower(), + self.subspecies.lower(), + ] + ) else: if self.kingdom == "": - return " ".join([ - self.genus.capitalize(), - self.species.lower(), - ]) + return " ".join( + [ + self.genus.capitalize(), + self.species.lower(), + ] + ) else: - return " ".join([ - self.kingdom.capitalize(), - self.phylum.capitalize(), - self.cls.capitalize(), - self.order.capitalize(), - self.family.capitalize(), - self.genus.capitalize(), - self.species.lower(), - ]) + return " ".join( + [ + self.kingdom.capitalize(), + self.phylum.capitalize(), + self.cls.capitalize(), + self.order.capitalize(), + self.family.capitalize(), + self.genus.capitalize(), + self.species.lower(), + ] + ) + @Property def get_common_name(self): return self.common_name @Property def sci_common_name(self): - if self.common_name == '': + if self.common_name == "": return self.scientific_name else: - return self.scientific_name + ' with common name ' + self.get_common_name - + return self.scientific_name + " with common name " + self.get_common_name + @Property def taxon_common_name(self): - if self.common_name == '': + if self.common_name == "": return self.taxonomic_name else: - return self.taxonomic_name + ' with common name ' + self.get_common_name + return self.taxonomic_name + " with common name " + self.get_common_name -def to_classes(data,text_type): - if text_type == 'asis': - return data['class'] + +def to_classes(data, text_type): + if text_type == "asis": + return data["class"] data_view = data.drop(columns=list(set(data.keys()) - set(Taxon.__dict__.keys()))) - if text_type == 'sci': - return data_view.apply(lambda x: Taxon(**x.to_dict()).scientific_name, axis=1).values.tolist() - elif text_type == 'taxon': - return data_view.apply(lambda x: Taxon(**x.to_dict()).taxonomic_name, axis=1).values.tolist() - elif text_type == 'com': - return data_view.apply(lambda x: Taxon(**x.to_dict()).get_common_name, axis=1).values.tolist() - elif text_type == 'sci_com': + if text_type == "sci": + return data_view.apply( + lambda x: Taxon(**x.to_dict()).scientific_name, axis=1 + ).values.tolist() + elif text_type == "taxon": + return data_view.apply( + lambda x: Taxon(**x.to_dict()).taxonomic_name, axis=1 + ).values.tolist() + elif text_type == "com": + return data_view.apply( + lambda x: Taxon(**x.to_dict()).get_common_name, axis=1 + ).values.tolist() + elif text_type == "sci_com": return data_view.apply(lambda x: Taxon(**x.to_dict()).sci_common_name, axis=1) - elif text_type == 'taxon_com': + elif text_type == "taxon_com": return data_view.apply(lambda x: Taxon(**x.to_dict()).taxon_common_name, axis=1) else: raise ValueError("text type is not acceptable.")
/home/runner/work/RCME/RCME/rcme/data/bioclip/src/imageomics/naming_reproduce.py#L1
import dataclasses import json import logging import re - logger = logging.getLogger() taxon_ranks = ("kingdom", "phylum", "cls", "order", "family", "genus", "species") + def clean_rank(value): assert isinstance(value, str) # Remove HTML value = strip_html(value).lower()
/home/runner/work/RCME/RCME/rcme/data/bioclip/src/imageomics/naming_reproduce.py#L227
else: return current return current + def strip_html(string): """Just removes <i>...</i> tags and similar. Not complete.""" def clean(s): return re.sub(r"<.*?>(.*?)</.*?>", r"\1", s) while string != clean(string): string = clean(string) return string + def clean_name(name): name = strip_html(name.lower()).strip() patterns = [
/home/runner/work/RCME/RCME/rcme/data/bioclip/src/imageomics/naming_reproduce.py#L262
start, end = match.span() name = name[:start].strip() return name + def load_name_lookup(path, keytype=str): """ Returns dict[key, (taxonomic, common)] """ with open(path) as fd: return { keytype(key): (Taxon(*taxa), common, classes) for key, (taxa, common, classes) in json.load(fd).items() } + def get_common(taxon, common): if common: return common if taxon.scientific: return taxon.scientific
/home/runner/work/RCME/RCME/rcme/models/lorentz.py#L17
All functions implemented here only input/output the space components, while while calculating the time component according to the Hyperboloid constraint: `x_time = torch.sqrt(1 / curv + torch.norm(x_space) ** 2)` """ + from __future__ import annotations import math import torch
/home/runner/work/RCME/RCME/rcme/train.py#L2
from data.dataset import ImageTextDataset, ImageTextDatasetMERU import pytorch_lightning as pl from pytorch_lightning.callbacks import ModelCheckpoint from argparse import ArgumentParser + def main(args): if args.model == "rcme": from models.rcme import RCME + train_dataset = ImageTextDataset(cfg.dataset_root, cfg.train_csv, mode="train") val_dataset = ImageTextDataset(cfg.dataset_root, cfg.val_csv, mode="val") model = RCME(cfg.rcme, train_dataset, val_dataset) elif args.model == "radial": from models.radial_embed_model import RadialEmbed + train_dataset = ImageTextDataset(cfg.dataset_root, cfg.train_csv, mode="train") val_dataset = ImageTextDataset(cfg.dataset_root, cfg.val_csv, mode="val") model = RadialEmbed(cfg.radial, train_dataset, val_dataset) elif args.model == "meru": from models.meru import MERU - train_dataset = ImageTextDatasetMERU(cfg.dataset_root, cfg.train_csv, mode="train") + + train_dataset = ImageTextDatasetMERU( + cfg.dataset_root, cfg.train_csv, mode="train" + ) val_dataset = ImageTextDatasetMERU(cfg.dataset_root, cfg.val_csv, mode="val") model = MERU(cfg.meru, train_dataset, val_dataset) elif args.model == "atmg": from models.atmg import ATMG - train_dataset = ImageTextDatasetMERU(cfg.dataset_root, cfg.train_csv, mode="train") + + train_dataset = ImageTextDatasetMERU( + cfg.dataset_root, cfg.train_csv, mode="train" + ) val_dataset = ImageTextDatasetMERU(cfg.dataset_root, cfg.val_csv, mode="val") model = ATMG(cfg.atmg, train_dataset, val_dataset) else: raise NotImplementedError(f"Model {args.model} not implemented") - + checkpoint_callback = ModelCheckpoint( - monitor='val_loss', - dirpath=f'checkpoints/{args.model}', - filename='{args.model}-{epoch:02d}-{val_loss:.2f}', + monitor="val_loss", + dirpath=f"checkpoints/{args.model}", + filename="{args.model}-{epoch:02d}-{val_loss:.2f}", save_top_k=3, - mode='min', + mode="min", save_last=True, ) - trainer = pl.Trainer(accelerator='gpu', devices=cfg[args.model].gpus, max_epochs=cfg[args.model].max_epochs, strategy='ddp', callbacks=[checkpoint_callback], - accumulate_grad_batches=cfg[args.model].accumulate_grad_batches, val_check_interval=0.1, limit_val_batches=100) + trainer = pl.Trainer( + accelerator="gpu", + devices=cfg[args.model].gpus, + max_epochs=cfg[args.model].max_epochs, + strategy="ddp", + callbacks=[checkpoint_callback], + accumulate_grad_batches=cfg[args.model].accumulate_grad_batches, + val_check_interval=0.1, + limit_val_batches=100, + ) trainer.fit(model) + if __name__ == "__main__": parser = ArgumentParser() - parser.add_argument("--model", type=str, default="rcme", choices=["rcme", "radial", "meru", "atmg"]) + parser.add_argument( + "--model", type=str, default="rcme", choices=["rcme", "radial", "meru", "atmg"] + ) args = parser.parse_args() - main(args) + main(args)
/home/runner/work/RCME/RCME/rcme/data/bioclip/write_imgs.py#L1
""" Writes the training and validation data to folder structure organized by taxonomic hierarchy. """ + import sys + sys.path.append("/projects/bdbl/ssastry/bioclip/src") import argparse import collections import csv import json
/home/runner/work/RCME/RCME/rcme/data/bioclip/write_imgs.py#L13
import tarfile import threading import tqdm from PIL import Image, ImageFile -from imageomics import disk_reproduce, eol_reproduce, evobio10m_reproduce, naming_reproduce, wds +from imageomics import ( + disk_reproduce, + eol_reproduce, + evobio10m_reproduce, + naming_reproduce, + wds, +) ######## # CONFIG ########
/home/runner/work/RCME/RCME/rcme/data/bioclip/write_imgs.py#L62
def create_taxonomic_folder_name(taxon): """Create folder name from taxonomic hierarchy with incrementing counter.""" global species_counter, species_folder_map - - if not hasattr(taxon, 'tagged') or not taxon.tagged: + + if not hasattr(taxon, "tagged") or not taxon.tagged: return None - + # Extract taxonomic levels from tagged data # Assuming tagged is a list of (level, value) tuples # We need to get all levels from kingdom to species - taxonomic_levels = ['kingdom', 'phylum', 'class', 'order', 'family', 'genus', 'species'] + taxonomic_levels = [ + "kingdom", + "phylum", + "class", + "order", + "family", + "genus", + "species", + ] folder_parts = [] - + for level, value in taxon.tagged: if level.lower() in taxonomic_levels: folder_parts.append(f"{value}") - + # Check if we have all required levels if len(folder_parts) < 7: # Need all 7 levels return None - + # Create species key for mapping - species_key = '_'.join(folder_parts) - + species_key = "_".join(folder_parts) + # Thread-safe access to species counter and map with csv_lock: # Check if we've seen this species before if species_key in species_folder_map: return species_folder_map[species_key] - + # This is a new species, increment counter and create folder name species_counter += 1 folder_name = f"{species_counter:06d}_{species_key}" species_folder_map[species_key] = folder_name - + return folder_name def get_taxonomic_info(taxon): """Extract taxonomic information for CSV.""" - if not hasattr(taxon, 'tagged') or not taxon.tagged: + if not hasattr(taxon, "tagged") or not taxon.tagged: return None, None, None, None, None, None, None - - taxonomic_levels = ['kingdom', 'phylum', 'class', 'order', 'family', 'genus', 'species'] + + taxonomic_levels = [ + "kingdom", + "phylum", + "class", + "order", + "family", + "genus", + "species", + ] taxon_dict = {} - + for level, value in taxon.tagged: if level.lower() in taxonomic_levels: taxon_dict[level.lower()] = value - + return ( - taxon_dict.get('kingdom', ''), - taxon_dict.get('phylum', ''), - taxon_dict.get('class', ''), - taxon_dict.get('order', ''), - taxon_dict.get('family', ''), - taxon_dict.get('genus', ''), - taxon_dict.get('species', '') + taxon_dict.get("kingdom", ""), + taxon_dict.get("phylum", ""), + taxon_dict.get("class", ""), + taxon_dict.get("order", ""), + taxon_dict.get("family", ""), + taxon_dict.get("genus", ""), + taxon_dict.get("species", ""), ) def save_image_to_folder(img, global_id, folder_name, source_dataset, taxon, common): """Save image to appropriate folder and write to CSV.""" # Create folder path folder_path = os.path.join(outdir, folder_name) os.makedirs(folder_path, exist_ok=True) - + # Save image image_filename = f"{global_id}.jpg" image_path = os.path.join(folder_path, image_filename) img.save(image_path, "JPEG", quality=95) - + # Get taxonomic information - kingdom, phylum, class_name, order, family, genus, species = get_taxonomic_info(taxon) - + kingdom, phylum, class_name, order, family, genus, species = get_taxonomic_info( + taxon + ) + # Write to CSV (thread-safe) csv_path = os.path.join(outdir, "image_metadata.csv") with csv_lock: - with open(csv_path, 'a', newline='', encoding='utf-8') as csvfile: + with open(csv_path, "a", newline="", encoding="utf-8") as csvfile: writer = csv.writer(csvfile) - writer.writerow([ - global_id, - folder_name, - image_filename, - source_dataset, - os.path.join(folder_name, image_filename), - taxon.scientific, - common, - kingdom, - phylum, - class_name, - order, - family, - genus, - species - ]) + writer.writerow( + [ + global_id, + folder_name, + image_filename, + source_dataset, + os.path.join(folder_name, image_filename), + taxon.scientific, + common, + kingdom, + phylum, + class_name, + order, + family, + genus, + species, + ] + ) def has_complete_taxonomy(taxon): """Check if taxon has all required taxonomic levels.""" - if not hasattr(taxon, 'tagged') or not taxon.tagged: + if not hasattr(taxon, "tagged") or not taxon.tagged: return False - - required_levels = {'kingdom', 'phylum', 'class', 'order', 'family', 'genus', 'species'} + + required_levels = { + "kingdom", + "phylum", + "class", + "order", + "family", + "genus", + "species", + } found_levels = set() - + for level, value in taxon.tagged: if level.lower() in required_levels and value: found_levels.add(level.lower()) - + return len(found_levels) == len(required_levels) ###################### # Encyclopedia of Life
/home/runner/work/RCME/RCME/rcme/data/bioclip/write_imgs.py#L188
evobio10m_id: (content_id, page_id) for evobio10m_id, content_id, page_id in db.execute(select_stmt).fetchall() } db.close() - name_lookup = naming_reproduce.load_name_lookup(disk_reproduce.eol_name_lookup_json, keytype=int) + name_lookup = naming_reproduce.load_name_lookup( + disk_reproduce.eol_name_lookup_json, keytype=int + ) print(f"Loaded {len(name_lookup)} EOL names.") # r|gz indicates reading from a gzipped file, streaming only with tarfile.open(imgset_path, "r|gz") as tar: for i, member in tqdm.tqdm(enumerate(tar)): eol_img = eol_reproduce.ImageFilename.from_filename(member.name) if eol_img.raw in image_blacklist: continue - + # Match on treeoflife_id filename if eol_img.tol_id not in eol_ids_lookup: print(f"Can't find the tol_id {eol_img.tol_id}") logger.warning( "EvoBio10m ID missing. [tol_id: %s]",
/home/runner/work/RCME/RCME/rcme/data/bioclip/write_imgs.py#L209
continue # fetching page ID content_id, page_id = eol_ids_lookup[eol_img.tol_id] global_id = eol_img.tol_id - + # checking for global id in split if global_id not in splits[args.split] or global_id in finished_ids: # logger.info( # "Skipping global ID already in split or finished. [global_id: %s, split: %s]", # global_id, # args.split, # ) continue - + # checking for page id if page_id not in name_lookup: - logger.warning( - "Page ID missing in name lookup. [page_id: %s]", page_id - ) - continue - + logger.warning("Page ID missing in name lookup. [page_id: %s]", page_id) + continue + # using name lookup for taxon, common, classes taxon, common, classes = name_lookup[page_id] if taxon.scientific in species_blacklist: logger.info(
/home/runner/work/RCME/RCME/rcme/data/bioclip/write_imgs.py#L277
(filename, cls_num): evobio10m_id for evobio10m_id, filename, cls_num in db.execute(select_stmt).fetchall() } db.close() - name_lookup = naming_reproduce.load_name_lookup(disk_reproduce.inat21_name_lookup_json, keytype=int) + name_lookup = naming_reproduce.load_name_lookup( + disk_reproduce.inat21_name_lookup_json, keytype=int + ) clsdir_path = os.path.join(disk_reproduce.inat21_root_dir, clsdir) for i, filename in enumerate(os.listdir(clsdir_path)): filepath = os.path.join(clsdir_path, filename)
/home/runner/work/RCME/RCME/rcme/data/bioclip/write_imgs.py#L331
(part, filename): evobio10m_id for evobio10m_id, part, filename in db.execute(select_stmt).fetchall() } db.close() - name_lookup = naming_reproduce.load_name_lookup(disk_reproduce.bioscan_name_lookup_json) + name_lookup = naming_reproduce.load_name_lookup( + disk_reproduce.bioscan_name_lookup_json + ) partdir = os.path.join(disk_reproduce.bioscan_root_dir, f"part{part}") for i, filename in enumerate(os.listdir(partdir)): if (part, filename) not in evobio10m_id_lookup: logger.warning(
/home/runner/work/RCME/RCME/rcme/data/bioclip/write_imgs.py#L404
) parser.add_argument( "--split", choices=["train", "val", "train_small"], default="val" ) parser.add_argument("--tag", default="dev", help="The suffix for the directory.") - #FIXME: Currently, does not work with multiple processes. + # FIXME: Currently, does not work with multiple processes. parser.add_argument( "--workers", type=int, default=1, help="Number of processes to use." ) args = parser.parse_args()
/home/runner/work/RCME/RCME/rcme/data/bioclip/write_imgs.py#L418
os.makedirs(outdir, exist_ok=True) print(f"Writing images to {outdir}.") # Initialize CSV file csv_path = os.path.join(outdir, "image_metadata.csv") - + # Create CSV header - with open(csv_path, 'w', newline='', encoding='utf-8') as csvfile: + with open(csv_path, "w", newline="", encoding="utf-8") as csvfile: writer = csv.writer(csvfile) - writer.writerow(['global_id', 'folder_name', 'image_filename', 'source_dataset', 'relative_path', 'scientific_name', 'common_name', 'kingdom', 'phylum', 'class', 'order', 'family', 'genus', 'species']) + writer.writerow( + [ + "global_id", + "folder_name", + "image_filename", + "source_dataset", + "relative_path", + "scientific_name", + "common_name", + "kingdom", + "phylum", + "class", + "order", + "family", + "genus", + "species", + ] + ) # db_path = f"{evobio10m_reproduce.get_outdir(args.tag)}/mapping.sqlite" db_path = os.path.abspath(disk_reproduce.db) print(f"Using database at {db_path}.")
Run linters
Node.js 20 actions are deprecated. The following actions are running on Node.js 20 and may not work as expected: actions/checkout@v2, actions/setup-python@v5, wearerequired/lint-action@v2. Actions will be forced to run with Node.js 24 by default starting June 2nd, 2026. Node.js 20 will be removed from the runner on September 16th, 2026. Please check if updated versions of these actions are available that support Node.js 24. To opt into Node.js 24 now, set the FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true environment variable on the runner or in your workflow file. Once Node.js 24 becomes the default, you can temporarily opt out by setting ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. For more information see: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/