162def DownloadCompoundToxicity(compound_data: dict, page_folder_name: str):
163 """
164 Скачиваем данные о токсичности соединения по информации из JSON PubChem
165 и сохраняем их в CSV-файл.
166
167 Args:
168 compound_data (dict): словарь с информацией о соединении из JSON PubChem.
169 page_folder_name (str): путь к директории, в которой будет сохранен файл.
170 """
171
172 cid: str = ""
173
174 try:
175
176 cid = compound_data["LinkedRecords"]["CID"][0]
177
178
179 except KeyError:
180 v_logger.warning(
181 f"No 'cid' for 'sid': {compound_data['LinkedRecords']['SID'][0]}, skip."
182 )
183 v_logger.info("-", LogMode.VERBOSELY)
184
185 return
186
187
188
189 primary_sid: int | None
190 try:
191
192 primary_sid = int(compound_data["LinkedRecords"]["SID"][0])
193
194
195 except KeyError:
196 primary_sid = None
197
198
199 raw_table: str = compound_data["Data"][0]["Value"]["ExternalTableName"]
200 table_info: dict = {}
201
202
203 for row in raw_table.split("&"):
204 key, value = row.split("=")
205 table_info[key] = value
206
207
208 if table_info["query_type"] != "sid":
209 v_logger.LogException(ValueError(f"Unknown query type at page {page_folder_name}"))
210
211
212 sid = int(table_info["query"])
213
214
215 if primary_sid != sid:
216 v_logger.warning(f"Mismatch between 'primary_sid' ({primary_sid}) and 'sid' ({sid}).")
217
218
219 compound_name: str = f"compound_{sid}_toxicity"
220
221
222 compound_file_kg = f"{page_folder_name.format(unit_type='kg')}/{compound_name}"
223 compound_file_m3 = f"{page_folder_name.format(unit_type='m3')}/{compound_name}"
224
225
226 if os.path.exists(f"{compound_file_kg}.csv") or (
227 os.path.exists(f"{compound_file_m3}.csv") and config["skip_downloaded"]
228 ):
229 v_logger.info(f"{compound_name} is already downloaded, skip.", LogMode.VERBOSELY)
230 v_logger.info("-", LogMode.VERBOSELY)
231
232 return
233
234 v_logger.info(f"Downloading {compound_name}...", LogMode.VERBOSELY)
235
236
237 acute_effects = GetDataFrameFromUrl(
238 GetLinkFromSid(
239 sid=sid, collection=table_info["collection"], limit=toxicity_config["limit"]
240 ),
241 toxicity_config["sleep_time"],
242 )
243
244 @ReTry()
245 def GetMolecularWeightByCid(cid: str | int) -> str:
246 """
247 Получает молекулярный вес соединения из PubChem REST API, используя его CID.
248
249 Args:
250 cid (str | int): PubChem Compound Identifier (CID) соединения.
251
252 Returns:
253 str: молекулярный вес соединения в виде строки.
254 """
255
256
257 return GetResponse(
258 "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/"
259 f"{cid}/property/MolecularWeight/txt",
260 True,
261 None,
262 ).text.strip()
263
264 def CalcMolecularWeight(
265 df: pd.DataFrame,
266 id_column: str,
267 ) -> pd.DataFrame:
268 """
269 Вычисляет и добавляет столбец 'mw' (молекулярный вес) в pd.DataFrame.
270
271 Args:
272 df (pd.DataFrame): исходный pd.DataFrame.
273 id_column (str): название столбца, содержащего ID соединений.
274
275 Returns:
276 pd.DataFrame: модифицированный DataFrame с добавленным столбцом 'mw'.
277 """
278
279
280 unique_ids = df[id_column].dropna().unique()
281
282
283 if len(unique_ids) == 1:
284
285 mw = GetMolecularWeightByCid(unique_ids[0])
286
287
288 if mw is not None:
289
290 df["mw"] = mw
291
292 v_logger.info(f"Found 'mw' by '{id_column}'.", LogMode.VERBOSELY)
293
294
295 else:
296 v_logger.warning(
297 f"Could not retrieve molecular weight by '{id_column}' for {unique_ids[0]}."
298 )
299
300
301 elif len(unique_ids) == 0:
302 v_logger.warning(f"No '{id_column}' found for {unique_ids[0]}.")
303
304
305 else:
306 v_logger.warning(f"Non-unique 'mw' by {id_column} for {unique_ids[0]}.")
307
308
309 df["mw"] = df[id_column].apply(GetMolecularWeightByCid)
310
311
312 if df["mw"].isnull().any():
313 v_logger.warning(f"Some 'mw' could not be retrieved by {id_column}.")
314
315 return df
316
317 def ExtractDoseAndTime(df: pd.DataFrame, valid_units: list[str]) -> pd.DataFrame:
318 """
319 Преобразует DataFrame с данными о дозировках, извлекая числовое
320 значение, единицу измерения и период времени.
321
322 Args:
323 df (pd.DataFrame): таблица с колонкой "dose", содержащей
324 информацию о дозировках.
325 valid_units (list[str]): список допустимых единиц измерения дозы.
326
327 Returns:
328 DataFrame с тремя новыми колонками: "numeric_dose", "dose_value",
329 "time_period".
330 """
331
332 df = df.copy()
333
334 def ExtractDose(
335 dose_str: str, mw: float
336 ) -> tuple[float | None, str | None, str | None]:
337 """
338 Извлекает дозу, единицу измерения и период времени из строки
339 дозировки.
340
341 Args:
342 dose_str (str): строка, содержащая информацию о дозировке.
343 mw (float): молекулярная масса соединения.
344
345 Returns:
346 tuple[float | None, str | None, str | None]: кортеж, содержащий:
347 - числовую дозу (float или None, если извлечь не удалось).
348 - единицу измерения дозы (str или None, если извлечь не удалось).
349 - период времени (str или None, если извлечь не удалось).
350 """
351
352
353 if " " not in dose_str:
354 return None, None, None
355
356 num_dose: float | str | None = None
357 dose_unit: str | None = None
358 time_per: str | None = None
359
360 try:
361
362 if len(dose_str.split(" ")) != len(
363 ["dose_amount_str", "dose_and_time"]
364 ):
365 return None, None, None
366
367
368 dose_amount_str, dose_and_time = dose_str.split(" ")
369
370 num_dose = float(dose_amount_str)
371
372
373 except ValueError:
374 v_logger.warning(f"Unsupported dose string: {dose_str}", LogMode.VERBOSELY)
375 return None, None, None
376
377
378 match dose_str.count("/"):
379 case 1:
380
381 if dose_and_time.startswith("p"):
382 dose_unit, time_per = dose_and_time.split("/")
383 else:
384 dose_unit = dose_and_time
385 time_per = None
386
387 case 2:
388
389 dose_unit = "/".join(dose_and_time.split("/")[:-1])
390 time_per = dose_and_time.split("/")[-1]
391
392 case _:
393 return None, None, None
394
395
396 if dose_unit not in valid_units:
397 v_logger.warning(
398 f"Unsupported dose_unit (non-valid): {dose_unit}", LogMode.VERBOSELY
399 )
400 return None, None, None
401
402 unit_prefix: str = dose_unit
403 unit_suffix: str = "m3"
404
405
406 if dose_unit.count("/") > 0:
407 unit_prefix, unit_suffix = dose_unit.split("/")
408
409
410 if unit_suffix not in ("kg", "m3"):
411 v_logger.warning(
412 f"Unsupported dose_unit (suffix): {dose_unit}", LogMode.VERBOSELY
413 )
414 return None, None, None
415
416 unit_prefix = unit_prefix.lower()
417
418
419 conversions: dict[str, float] = {
420 "mg": 1,
421 "gm": 1000,
422 "g": 1000,
423 "ng": 0.000001,
424 "ug": 0.001,
425 "ml": 1000,
426 "nl": 0.001,
427 "ul": 1,
428 "ppm": 24.45 / mw,
429 "ppb": 0.001 * 24.45 / mw,
430 "pph": 1 / 60 * 24.45 / mw,
431 }
432
433
434 if unit_prefix in conversions:
435 num_dose *= conversions[unit_prefix]
436 dose_unit = "mg/" + unit_suffix
437
438
439 else:
440 v_logger.warning(
441 f"Unsupported dose_unit (prefix): {dose_unit}", LogMode.VERBOSELY
442 )
443 return None, None, None
444
445 return num_dose, dose_unit, time_per
446
447
448 df[["numeric_dose", "dose_units", "time_period"]] = df.apply(
449 lambda row: pd.Series(ExtractDose(row["dose"], row["mw"])), axis=1
450 )
451
452
453 df = df.drop(columns=["dose"]).rename(columns={"numeric_dose": "dose"})
454
455 return df
456
457 def SaveMolfileWithToxicityToSDF(df: pd.DataFrame, unit_type: str):
458 """
459 Сохраняет molfile соединения с данными о токсичности в SDF-файл.
460
461 Args:
462 df (pd.DataFrame): DataFrame, содержащий данные о токсичности соединения.
463 unit_type (str): тип единиц измерения (например, "kg" или "m3").
464 """
465
466
467 listed_df = pd.DataFrame()
468
469
470 for column_name in df.columns:
471
472 full_column_data = df[column_name].tolist()
473
474
475 listed_df[column_name] = [full_column_data]
476
477 if len(DedupedList(full_column_data)) == 1:
478
479 listed_df.loc[0, column_name] = full_column_data[0]
480
481
482 SaveMolfilesToSDF(
483 data=pd.DataFrame({"cid": [cid], "molfile": [GetMolfileFromCID(cid)]}),
484 file_name=(
485 f"{toxicity_config['molfiles_folder_name']}/{compound_name}_{unit_type}"
486 ),
487 molecule_id_column_name="cid",
488 extra_data=listed_df,
489 indexing_lists=True,
490 )
491
492 def SaveToxicityUnitSpecification(
493 compound_file_unit: str,
494 unit_str: str,
495 valid_units: list[str],
496 acute_effects: pd.DataFrame,
497 ):
498 """
499 Фильтрует, преобразует и сохраняет данные о токсичности для указанного
500 типа единиц измерения.
501
502 Args:
503 compound_file_unit (str): имя файла для сохранения (без расширения).
504 unit_str (str): тип единиц измерения ("kg" или "m3").
505 valid_units (list[str]): список допустимых единиц измерения.
506 acute_effects (pd.DataFrame): DataFrame с данными о токсичности.
507 """
508
509 v_logger.info(
510 f"Filtering by {list(filtering_config[unit_str].keys())}...", LogMode.VERBOSELY
511 )
512
513 acute_effects_unit: pd.DataFrame = acute_effects.copy()
514
515
516 for key in filtering_config[unit_str].keys():
517 if len(filtering_config[unit_str][key]) != 0:
518 acute_effects_unit = acute_effects_unit[
519 acute_effects_unit[key].isin(filtering_config[unit_str][key])
520 ]
521
522 v_logger.success(
523 f"Filtering by {list(filtering_config[unit_str].keys())}!", LogMode.VERBOSELY
524 )
525
526 v_logger.info(f"Filtering 'dose' in {unit_str}...", LogMode.VERBOSELY)
527
528
529 if acute_effects_unit.empty:
530 v_logger.warning(
531 f"{compound_name}_{unit_str} is empty, no need saving, skip.", LogMode.VERBOSELY
532 )
533 return
534
535
536 if "dose" in acute_effects_unit.columns:
537
538 acute_effects_unit = ExtractDoseAndTime(acute_effects_unit, valid_units)
539
540
541 acute_effects_unit["dose"] = pd.to_numeric(
542 acute_effects_unit["dose"], errors="coerce"
543 )
544
545
546 else:
547 v_logger.warning(f"No dose in {compound_name}_{unit_str}, skip.", LogMode.VERBOSELY)
548 return
549
550
551 if acute_effects_unit.empty:
552 v_logger.warning(
553 f"{compound_name}_{unit_str} is empty, no need saving, skip.", LogMode.VERBOSELY
554 )
555 return
556
557
558 if (
559 "dose" not in acute_effects_unit.columns
560 or "dose_units" not in acute_effects_unit.columns
561 ):
562 v_logger.warning(
563 f"{compound_name}_{unit_str} misses 'dose' or 'dose_units', skip.",
564 LogMode.VERBOSELY,
565 )
566 return
567
568 v_logger.success(f"Filtering 'dose' in {unit_str}!", LogMode.VERBOSELY)
569
570 v_logger.info(f"Adding 'pLD' to {compound_name}_{unit_str}...", LogMode.VERBOSELY)
571
572
573 acute_effects_unit["pLD"] = -np.log10(
574 (acute_effects_unit["dose"] / acute_effects_unit["mw"]) / 1000000
575 )
576
577 v_logger.success(f"Adding 'pLD' to {compound_name}_{unit_str}!", LogMode.VERBOSELY)
578
579 v_logger.info(f"Saving {compound_name}_{unit_str} to .csv...", LogMode.VERBOSELY)
580
581
582 acute_effects_unit = acute_effects_unit.replace("", np.nan)
583
584 acute_effects_unit = acute_effects_unit.dropna(axis=1, how="all")
585
586
587 if (
588 "dose" in acute_effects_unit.columns and "dose_units" in acute_effects_unit.columns
589 ):
590
591
592 acute_effects_unit = acute_effects_unit[
593 (acute_effects_unit["dose_units"].notna()) & (acute_effects_unit["dose"].notna())
594 ]
595
596
597 else:
598 v_logger.warning(
599 f"{compound_name}_{unit_str} misses 'dose' or 'dose_units', skip.",
600 LogMode.VERBOSELY,
601 )
602 return
603
604
605 acute_effects_unit.to_csv(f"{compound_file_unit}.csv", sep=";", index=False, mode="w")
606
607 v_logger.success(f"Saving {compound_name}_{unit_str} to .csv!", LogMode.VERBOSELY)
608
609
610 if toxicity_config["download_compounds_sdf"]:
611 v_logger.info(f"Saving {compound_name}_{unit_str} to .sdf...", LogMode.VERBOSELY)
612
613
614 os.makedirs(toxicity_config["molfiles_folder_name"], exist_ok=True)
615
616
617 SaveMolfileWithToxicityToSDF(acute_effects_unit, unit_str)
618
619 v_logger.success(f"Saving {compound_name}_{unit_str} to .sdf!", LogMode.VERBOSELY)
620
621 v_logger.info("Adding 'mw'...", LogMode.VERBOSELY)
622
623
624 acute_effects = CalcMolecularWeight(acute_effects, "cid")
625
626 try:
627
628 acute_effects["mw"] = pd.to_numeric(acute_effects["mw"], errors="coerce")
629
630 v_logger.success("Adding 'mw'!", LogMode.VERBOSELY)
631
632
633 except KeyError:
634 v_logger.warning(f"No 'mw' for {compound_name}, skip.")
635 return
636
637 v_logger.info("~", LogMode.VERBOSELY)
638
639
640 SaveToxicityUnitSpecification(
641 compound_file_unit=compound_file_kg,
642 unit_str="kg",
643 valid_units=["gm/kg", "g/kg", "mg/kg", "ug/kg", "ng/kg", "mL/kg", "uL/kg", "nL/kg"],
644 acute_effects=acute_effects,
645 )
646
647 v_logger.info("·", LogMode.VERBOSELY)
648
649
650 SaveToxicityUnitSpecification(
651 compound_file_unit=compound_file_m3,
652 unit_str="m3",
653 valid_units=[
654 "gm/m3",
655 "g/m3",
656 "mg/m3",
657 "ug/m3",
658 "ng/m3",
659 "mL/m3",
660 "uL/m3",
661 "nL/m3",
662 "ppm",
663 "ppb",
664 "pph",
665 ],
666 acute_effects=acute_effects,
667 )
668
669 v_logger.info("·", LogMode.VERBOSELY)
670 v_logger.success(f"Downloading {compound_name}!", LogMode.VERBOSELY)
671 v_logger.info("-", LogMode.VERBOSELY)