polynet 1.0.2__tar.gz → 1.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. {polynet-1.0.2 → polynet-1.0.3}/PKG-INFO +7 -3
  2. {polynet-1.0.2 → polynet-1.0.3}/README.md +4 -1
  3. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/components/forms/explain_model.py +4 -5
  4. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/components/forms/explain_tml.py +4 -5
  5. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/components/forms/representation.py +43 -65
  6. polynet-1.0.3/polynet/app/components/select_all.py +73 -0
  7. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/options/state_keys.py +2 -0
  8. {polynet-1.0.2 → polynet-1.0.3}/polynet/featurizer/descriptors.py +7 -1
  9. {polynet-1.0.2 → polynet-1.0.3}/polynet/pipeline/self_check.py +67 -8
  10. {polynet-1.0.2 → polynet-1.0.3}/polynet/visualization/plots.py +1 -1
  11. {polynet-1.0.2 → polynet-1.0.3}/pyproject.toml +5 -2
  12. {polynet-1.0.2 → polynet-1.0.3}/LICENSE +0 -0
  13. {polynet-1.0.2 → polynet-1.0.3}/polynet/__init__.py +0 -0
  14. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/Welcome_to_PolyNet.py +0 -0
  15. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/components/experiments.py +0 -0
  16. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/components/figure_export.py +0 -0
  17. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/components/forms/analyse_results.py +0 -0
  18. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/components/forms/applicability_domain.py +0 -0
  19. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/components/forms/create_experiment.py +0 -0
  20. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/components/forms/explanation_plots.py +0 -0
  21. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/components/forms/fragment_plots.py +0 -0
  22. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/components/forms/plot_customiser.py +0 -0
  23. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/components/forms/search_grid.py +0 -0
  24. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/components/forms/train_models.py +0 -0
  25. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/components/graph_display.py +0 -0
  26. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/components/optional_dependencies.py +0 -0
  27. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/components/plots.py +0 -0
  28. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/components/version_footer.py +0 -0
  29. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/options/allowable_sets.py +0 -0
  30. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/options/file_paths.py +0 -0
  31. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/options/plot.py +0 -0
  32. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/pages/1_Create_Experiment.py +0 -0
  33. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/pages/2_Representation.py +0 -0
  34. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/pages/3_Train_Models.py +0 -0
  35. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/pages/4_Predict.py +0 -0
  36. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/pages/5_Explain_Models.py +0 -0
  37. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/pages/6_Analyse_Results.py +0 -0
  38. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/services/cache.py +0 -0
  39. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/services/configurations.py +0 -0
  40. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/services/experiments.py +0 -0
  41. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/services/explain_model.py +0 -0
  42. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/services/explain_source.py +0 -0
  43. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/services/explain_tml.py +0 -0
  44. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/services/explanation_results.py +0 -0
  45. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/services/graph_viz.py +0 -0
  46. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/services/model_training.py +0 -0
  47. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/services/predict_model.py +0 -0
  48. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/services/progress.py +0 -0
  49. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/services/reruns.py +0 -0
  50. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/services/workload.py +0 -0
  51. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/static/polynet.png +0 -0
  52. {polynet-1.0.2 → polynet-1.0.3}/polynet/app/utils/__init__.py +0 -0
  53. {polynet-1.0.2 → polynet-1.0.3}/polynet/applicability/__init__.py +0 -0
  54. {polynet-1.0.2 → polynet-1.0.3}/polynet/applicability/assess.py +0 -0
  55. {polynet-1.0.2 → polynet-1.0.3}/polynet/applicability/calibration.py +0 -0
  56. {polynet-1.0.2 → polynet-1.0.3}/polynet/applicability/domain.py +0 -0
  57. {polynet-1.0.2 → polynet-1.0.3}/polynet/applicability/reference.py +0 -0
  58. {polynet-1.0.2 → polynet-1.0.3}/polynet/applicability/similarity.py +0 -0
  59. {polynet-1.0.2 → polynet-1.0.3}/polynet/cli.py +0 -0
  60. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/_loader.py +0 -0
  61. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/column_names.py +0 -0
  62. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/constants.py +0 -0
  63. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/display_names.py +0 -0
  64. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/enums.py +0 -0
  65. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/experiment.py +0 -0
  66. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/from_app.py +0 -0
  67. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/from_yaml.py +0 -0
  68. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/io.py +0 -0
  69. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/paths.py +0 -0
  70. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/schemas/__init__.py +0 -0
  71. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/schemas/applicability_domain.py +0 -0
  72. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/schemas/base.py +0 -0
  73. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/schemas/data.py +0 -0
  74. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/schemas/explainability.py +0 -0
  75. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/schemas/feature_preprocessing.py +0 -0
  76. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/schemas/fingerprints.py +0 -0
  77. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/schemas/general.py +0 -0
  78. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/schemas/plotting.py +0 -0
  79. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/schemas/representation.py +0 -0
  80. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/schemas/split_data.py +0 -0
  81. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/schemas/target_preprocessing.py +0 -0
  82. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/schemas/tml_explainability.py +0 -0
  83. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/schemas/training.py +0 -0
  84. {polynet-1.0.2 → polynet-1.0.3}/polynet/config/search_grid.py +0 -0
  85. {polynet-1.0.2 → polynet-1.0.3}/polynet/data/__init__.py +0 -0
  86. {polynet-1.0.2 → polynet-1.0.3}/polynet/data/benchmarks/fluorine_mri_snr.csv +0 -0
  87. {polynet-1.0.2 → polynet-1.0.3}/polynet/data/creator.py +0 -0
  88. {polynet-1.0.2 → polynet-1.0.3}/polynet/data/feature_transformer.py +0 -0
  89. {polynet-1.0.2 → polynet-1.0.3}/polynet/data/loader.py +0 -0
  90. {polynet-1.0.2 → polynet-1.0.3}/polynet/data/preprocessing.py +0 -0
  91. {polynet-1.0.2 → polynet-1.0.3}/polynet/data/sampling.py +0 -0
  92. {polynet-1.0.2 → polynet-1.0.3}/polynet/data/structures.py +0 -0
  93. {polynet-1.0.2 → polynet-1.0.3}/polynet/experiment/__init__.py +0 -0
  94. {polynet-1.0.2 → polynet-1.0.3}/polynet/experiment/manager.py +0 -0
  95. {polynet-1.0.2 → polynet-1.0.3}/polynet/explainability/__init__.py +0 -0
  96. {polynet-1.0.2 → polynet-1.0.3}/polynet/explainability/attributions.py +0 -0
  97. {polynet-1.0.2 → polynet-1.0.3}/polynet/explainability/embeddings.py +0 -0
  98. {polynet-1.0.2 → polynet-1.0.3}/polynet/explainability/explain.py +0 -0
  99. {polynet-1.0.2 → polynet-1.0.3}/polynet/explainability/fragment_figures.py +0 -0
  100. {polynet-1.0.2 → polynet-1.0.3}/polynet/explainability/masking.py +0 -0
  101. {polynet-1.0.2 → polynet-1.0.3}/polynet/explainability/selection.py +0 -0
  102. {polynet-1.0.2 → polynet-1.0.3}/polynet/explainability/shap_explain.py +0 -0
  103. {polynet-1.0.2 → polynet-1.0.3}/polynet/explainability/visualization.py +0 -0
  104. {polynet-1.0.2 → polynet-1.0.3}/polynet/factories/__init__.py +0 -0
  105. {polynet-1.0.2 → polynet-1.0.3}/polynet/factories/dataloader.py +0 -0
  106. {polynet-1.0.2 → polynet-1.0.3}/polynet/factories/loss.py +0 -0
  107. {polynet-1.0.2 → polynet-1.0.3}/polynet/factories/network.py +0 -0
  108. {polynet-1.0.2 → polynet-1.0.3}/polynet/factories/optimizer.py +0 -0
  109. {polynet-1.0.2 → polynet-1.0.3}/polynet/featurizer/__init__.py +0 -0
  110. {polynet-1.0.2 → polynet-1.0.3}/polynet/featurizer/allowable_sets.py +0 -0
  111. {polynet-1.0.2 → polynet-1.0.3}/polynet/featurizer/graph.py +0 -0
  112. {polynet-1.0.2 → polynet-1.0.3}/polynet/featurizer/pmx.py +0 -0
  113. {polynet-1.0.2 → polynet-1.0.3}/polynet/featurizer/polymer_graph.py +0 -0
  114. {polynet-1.0.2 → polynet-1.0.3}/polynet/featurizer/selection.py +0 -0
  115. {polynet-1.0.2 → polynet-1.0.3}/polynet/inference/__init__.py +0 -0
  116. {polynet-1.0.2 → polynet-1.0.3}/polynet/inference/ensemble.py +0 -0
  117. {polynet-1.0.2 → polynet-1.0.3}/polynet/inference/gnn.py +0 -0
  118. {polynet-1.0.2 → polynet-1.0.3}/polynet/inference/predict.py +0 -0
  119. {polynet-1.0.2 → polynet-1.0.3}/polynet/inference/tml.py +0 -0
  120. {polynet-1.0.2 → polynet-1.0.3}/polynet/inference/utils.py +0 -0
  121. {polynet-1.0.2 → polynet-1.0.3}/polynet/models/__init__.py +0 -0
  122. {polynet-1.0.2 → polynet-1.0.3}/polynet/models/base.py +0 -0
  123. {polynet-1.0.2 → polynet-1.0.3}/polynet/models/gnn/__init__.py +0 -0
  124. {polynet-1.0.2 → polynet-1.0.3}/polynet/models/gnn/cgcnn.py +0 -0
  125. {polynet-1.0.2 → polynet-1.0.3}/polynet/models/gnn/cggnn.py +0 -0
  126. {polynet-1.0.2 → polynet-1.0.3}/polynet/models/gnn/gat.py +0 -0
  127. {polynet-1.0.2 → polynet-1.0.3}/polynet/models/gnn/gcn.py +0 -0
  128. {polynet-1.0.2 → polynet-1.0.3}/polynet/models/gnn/graphsage.py +0 -0
  129. {polynet-1.0.2 → polynet-1.0.3}/polynet/models/gnn/mpnn.py +0 -0
  130. {polynet-1.0.2 → polynet-1.0.3}/polynet/models/gnn/transformer.py +0 -0
  131. {polynet-1.0.2 → polynet-1.0.3}/polynet/models/persistence.py +0 -0
  132. {polynet-1.0.2 → polynet-1.0.3}/polynet/pipeline/__init__.py +0 -0
  133. {polynet-1.0.2 → polynet-1.0.3}/polynet/pipeline/runner.py +0 -0
  134. {polynet-1.0.2 → polynet-1.0.3}/polynet/pipeline/stages.py +0 -0
  135. {polynet-1.0.2 → polynet-1.0.3}/polynet/plotting/data_analysis.py +0 -0
  136. {polynet-1.0.2 → polynet-1.0.3}/polynet/training/__init__.py +0 -0
  137. {polynet-1.0.2 → polynet-1.0.3}/polynet/training/cv.py +0 -0
  138. {polynet-1.0.2 → polynet-1.0.3}/polynet/training/evaluate.py +0 -0
  139. {polynet-1.0.2 → polynet-1.0.3}/polynet/training/gnn.py +0 -0
  140. {polynet-1.0.2 → polynet-1.0.3}/polynet/training/hyperopt.py +0 -0
  141. {polynet-1.0.2 → polynet-1.0.3}/polynet/training/metrics.py +0 -0
  142. {polynet-1.0.2 → polynet-1.0.3}/polynet/training/tml.py +0 -0
  143. {polynet-1.0.2 → polynet-1.0.3}/polynet/utils/__init__.py +0 -0
  144. {polynet-1.0.2 → polynet-1.0.3}/polynet/utils/chem_utils.py +0 -0
  145. {polynet-1.0.2 → polynet-1.0.3}/polynet/utils/data_preprocessing.py +0 -0
  146. {polynet-1.0.2 → polynet-1.0.3}/polynet/utils/graph_analysis.py +0 -0
  147. {polynet-1.0.2 → polynet-1.0.3}/polynet/utils/optional_dependencies.py +0 -0
  148. {polynet-1.0.2 → polynet-1.0.3}/polynet/utils/statistical_analysis.py +0 -0
  149. {polynet-1.0.2 → polynet-1.0.3}/polynet/utils/validation.py +0 -0
  150. {polynet-1.0.2 → polynet-1.0.3}/polynet/visualization/__init__.py +0 -0
  151. {polynet-1.0.2 → polynet-1.0.3}/polynet/visualization/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polynet
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: A Python package for polymer informatics: property prediction with graph neural networks and traditional machine learning
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -30,7 +30,8 @@ Requires-Dist: shap (>=0.51.0,<0.52.0)
30
30
  Requires-Dist: statsmodels (>=0.14.5,<0.15.0)
31
31
  Requires-Dist: streamlit (>=1.45.1,<2.0.0)
32
32
  Requires-Dist: torch (==2.2.0) ; sys_platform == "darwin" and platform_machine == "x86_64"
33
- Requires-Dist: torch (>=2.5.1,<3.0.0) ; sys_platform != "darwin" or platform_machine != "x86_64"
33
+ Requires-Dist: torch (>=2.5.1,<2.11) ; sys_platform == "darwin" and platform_machine == "arm64"
34
+ Requires-Dist: torch (>=2.5.1,<3.0.0) ; sys_platform != "darwin"
34
35
  Requires-Dist: torch-geometric (==2.7)
35
36
  Requires-Dist: xgboost (>=3.0.2,<4.0.0)
36
37
  Project-URL: Changelog, https://github.com/Biomaterials-for-Medical-Devices-AI/PolyNet/blob/main/CHANGELOG.md
@@ -129,6 +130,9 @@ Then install PolyNet:
129
130
  pip install polynet
130
131
  ```
131
132
 
133
+ On macOS, XGBoost also needs the OpenMP library from [Homebrew](https://brew.sh):
134
+ `brew install libomp`.
135
+
132
136
  This installs the `polynet` command: `polynet` opens the GUI and `polynet run --config
133
137
  experiment.yaml` runs the pipeline (see Quick Start below).
134
138
 
@@ -324,7 +328,7 @@ If you use PolyNet, please cite the software (GitHub's "Cite this repository" bu
324
328
  @software{polynet,
325
329
  author = {Aguilar-Bejarano, Eduardo and Alexander, Morgan and Winkler, David and Figueredo, Grazziela},
326
330
  title = {PolyNet: A Python Package for Polymer Informatics},
327
- version = {1.0.2},
331
+ version = {1.0.3},
328
332
  year = {2026},
329
333
  doi = {10.5281/zenodo.23302984},
330
334
  url = {https://github.com/Biomaterials-for-Medical-Devices-AI/PolyNet}
@@ -89,6 +89,9 @@ Then install PolyNet:
89
89
  pip install polynet
90
90
  ```
91
91
 
92
+ On macOS, XGBoost also needs the OpenMP library from [Homebrew](https://brew.sh):
93
+ `brew install libomp`.
94
+
92
95
  This installs the `polynet` command: `polynet` opens the GUI and `polynet run --config
93
96
  experiment.yaml` runs the pipeline (see Quick Start below).
94
97
 
@@ -284,7 +287,7 @@ If you use PolyNet, please cite the software (GitHub's "Cite this repository" bu
284
287
  @software{polynet,
285
288
  author = {Aguilar-Bejarano, Eduardo and Alexander, Morgan and Winkler, David and Figueredo, Grazziela},
286
289
  title = {PolyNet: A Python Package for Polymer Informatics},
287
- version = {1.0.2},
290
+ version = {1.0.3},
288
291
  year = {2026},
289
292
  doi = {10.5281/zenodo.23302984},
290
293
  url = {https://github.com/Biomaterials-for-Medical-Devices-AI/PolyNet}
@@ -5,6 +5,7 @@ from rdkit.Chem import Descriptors
5
5
  import streamlit as st
6
6
  from torch_geometric.data import Dataset
7
7
 
8
+ from polynet.app.components.select_all import multiselect_with_select_all
8
9
  from polynet.app.options.state_keys import ExplainModelStateKeys, ProjectionPlotStateKeys
9
10
  from polynet.app.services.explain_model import (
10
11
  analyse_graph_embeddings,
@@ -252,14 +253,12 @@ def _shared_params_section(
252
253
  all_archs = sorted({_parse_gnn_filename(f)[0] for f in gnn_models})
253
254
  all_iterations = sorted({_parse_gnn_filename(f)[1] for f in gnn_models})
254
255
 
255
- select_all = st.toggle(
256
- "Select all GNN models", key=ExplainModelStateKeys.SelectAllModels, value=False
257
- )
258
- selected_archs = st.multiselect(
256
+ selected_archs = multiselect_with_select_all(
259
257
  "Select GNN architectures to explain",
260
258
  options=all_archs,
261
- default=all_archs if select_all else None,
262
259
  key=ExplainModelStateKeys.GNNArchSelector,
260
+ select_all_label="Select all GNN models",
261
+ select_all_key=ExplainModelStateKeys.SelectAllModels,
263
262
  )
264
263
  selected_iterations = st.multiselect(
265
264
  "Select bootstrap iterations",
@@ -16,6 +16,7 @@ import pandas as pd
16
16
  import streamlit as st
17
17
 
18
18
  from polynet.app.components.forms.explain_model import explain_mols_widget
19
+ from polynet.app.components.select_all import multiselect_with_select_all
19
20
  from polynet.app.options.state_keys import TMLExplainStateKeys
20
21
  from polynet.app.services.explain_tml import explain_tml_global, explain_tml_local
21
22
  from polynet.app.services.reruns import explanation_rerun_button
@@ -58,14 +59,12 @@ def _tml_shared_params_section(
58
59
  )
59
60
  all_iterations = sorted({int(key.rsplit("_", 1)[1]) for key in tml_model_log_names})
60
61
 
61
- select_all = st.toggle(
62
- "Select all TML models", key=TMLExplainStateKeys.SelectAllTMLModels, value=False
63
- )
64
- selected_model_types = st.multiselect(
62
+ selected_model_types = multiselect_with_select_all(
65
63
  "Select TML model types to explain",
66
64
  options=all_model_types,
67
- default=all_model_types if select_all else None,
68
65
  key=TMLExplainStateKeys.TMLModelSelector,
66
+ select_all_label="Select all TML models",
67
+ select_all_key=TMLExplainStateKeys.SelectAllTMLModels,
69
68
  )
70
69
 
71
70
  selected_reprs = st.multiselect(
@@ -5,6 +5,7 @@ from rdkit import Chem
5
5
  from rdkit.Chem import Descriptors
6
6
  import streamlit as st
7
7
 
8
+ from polynet.app.components.select_all import multiselect_with_select_all
8
9
  from polynet.app.options.allowable_sets import atom_properties, bond_features
9
10
  from polynet.app.options.state_keys import DescriptorCalculationStateKeys
10
11
  from polynet.app.utils import keep_only_numerical_columns
@@ -277,23 +278,16 @@ def molecular_descriptor_representation(
277
278
 
278
279
  st.markdown("### Calculate RDKit Molecular Descriptors")
279
280
 
280
- select_all = st.checkbox(
281
- "Select all descriptors",
282
- value=not is_psmiles,
283
- key=DescriptorCalculationStateKeys.SelectAllRDKitDescriptors,
284
- help="If checked, all RDKit molecular descriptors will be selected by default. Uncheck to select specific descriptors.",
285
- )
286
-
287
- if select_all:
288
- default_descriptors = all_descriptors
289
- else:
290
- default_descriptors = defaults.get(MolecularDescriptor.RDKit, [])
291
-
292
- selected_descriptors = st.multiselect(
281
+ selected_descriptors = multiselect_with_select_all(
293
282
  "Select descriptors to calculate:",
294
283
  options=all_descriptors,
295
- default=default_descriptors, # Preselect common ones
296
284
  key=DescriptorCalculationStateKeys.DescriptorsRDKit,
285
+ select_all_label="Select all descriptors",
286
+ select_all_key=DescriptorCalculationStateKeys.SelectAllRDKitDescriptors,
287
+ select_all_default=not is_psmiles,
288
+ partial_selection=defaults.get(MolecularDescriptor.RDKit, []),
289
+ use_checkbox=True,
290
+ select_all_help="If checked, all RDKit molecular descriptors will be selected by default. Uncheck to select specific descriptors.",
297
291
  help="Choose which RDKit descriptors to compute for the molecules.",
298
292
  )
299
293
  st.info(f"You have selected {len((selected_descriptors))} RDKit descriptors.")
@@ -380,43 +374,37 @@ def molecular_descriptor_representation(
380
374
  BACKBONE_TOPOLOGICAL_FEATURIZER_REGISTRY.keys()
381
375
  )
382
376
 
383
- select_all_repeat_unit = st.checkbox(
384
- "Select all repeat-unit descriptors",
385
- value=True,
386
- key=DescriptorCalculationStateKeys.SelectAllPMXRepeatUnit,
387
- help="If checked, every repeat-unit descriptor is selected by default. Uncheck to choose specific descriptors.",
388
- )
389
- polymer_descriptors = st.multiselect(
377
+ polymer_descriptors = multiselect_with_select_all(
390
378
  "Select the descriptors to calculate for the monomer repeat unit.",
391
379
  options=repeat_unit_options,
392
- default=repeat_unit_options if select_all_repeat_unit else None,
393
380
  key=DescriptorCalculationStateKeys.PMXRepeatUnit,
381
+ select_all_label="Select all repeat-unit descriptors",
382
+ select_all_key=DescriptorCalculationStateKeys.SelectAllPMXRepeatUnit,
383
+ select_all_default=True,
384
+ use_checkbox=True,
385
+ select_all_help="If checked, every repeat-unit descriptor is selected by default. Uncheck to choose specific descriptors.",
394
386
  )
395
387
 
396
- select_all_side_chain = st.checkbox(
397
- "Select all side-chain descriptors",
398
- value=True,
399
- key=DescriptorCalculationStateKeys.SelectAllPMXSideChainDescriptors,
400
- help="If checked, every side-chain descriptor is selected by default. Uncheck to choose specific descriptors.",
401
- )
402
- side_chain_descriptors = st.multiselect(
388
+ side_chain_descriptors = multiselect_with_select_all(
403
389
  "Select the descriptors to calculate for the side chain of the polymers",
404
390
  options=side_chain_options,
405
- default=side_chain_options if select_all_side_chain else None,
406
391
  key=DescriptorCalculationStateKeys.PMXSideChainDescriptors,
392
+ select_all_label="Select all side-chain descriptors",
393
+ select_all_key=DescriptorCalculationStateKeys.SelectAllPMXSideChainDescriptors,
394
+ select_all_default=True,
395
+ use_checkbox=True,
396
+ select_all_help="If checked, every side-chain descriptor is selected by default. Uncheck to choose specific descriptors.",
407
397
  )
408
398
 
409
- select_all_backbone = st.checkbox(
410
- "Select all backbone descriptors",
411
- value=True,
412
- key=DescriptorCalculationStateKeys.SelectAllPMXBackboneDescriptors,
413
- help="If checked, every backbone descriptor is selected by default. Uncheck to choose specific descriptors.",
414
- )
415
- backbone_descriptors = st.multiselect(
399
+ backbone_descriptors = multiselect_with_select_all(
416
400
  "Select the descriptors to calculate for the backbone of the polymers",
417
401
  options=backbone_options,
418
- default=backbone_options if select_all_backbone else None,
419
402
  key=DescriptorCalculationStateKeys.PMXBackboneDescriptors,
403
+ select_all_label="Select all backbone descriptors",
404
+ select_all_key=DescriptorCalculationStateKeys.SelectAllPMXBackboneDescriptors,
405
+ select_all_default=True,
406
+ use_checkbox=True,
407
+ select_all_help="If checked, every backbone descriptor is selected by default. Uncheck to choose specific descriptors.",
420
408
  )
421
409
 
422
410
  agg_method = st.multiselect(
@@ -558,24 +546,20 @@ def graph_representation(
558
546
  "removed from the options below."
559
547
  )
560
548
 
561
- select_all_atomic = st.checkbox("Select all atomic properties")
562
-
563
- if select_all_atomic:
564
- default_atomic_properties = atomic_properties
565
- else:
566
- default_atomic_properties = [
549
+ selected_atomic_properties = multiselect_with_select_all(
550
+ "Select atomic properties to compute:",
551
+ options=atomic_properties,
552
+ key=DescriptorCalculationStateKeys.AtomProperties,
553
+ select_all_label="Select all atomic properties",
554
+ select_all_key=DescriptorCalculationStateKeys.SelectAllAtomProperties,
555
+ partial_selection=[
567
556
  AtomFeature.GetAtomicNum,
568
557
  AtomFeature.GetTotalNumHs,
569
558
  AtomFeature.GetTotalDegree,
570
559
  AtomFeature.GetImplicitValence,
571
560
  AtomFeature.GetIsAromatic,
572
- ]
573
-
574
- selected_atomic_properties = st.multiselect(
575
- "Select atomic properties to compute:",
576
- options=atomic_properties,
577
- default=default_atomic_properties,
578
- key=DescriptorCalculationStateKeys.AtomProperties,
561
+ ],
562
+ use_checkbox=True,
579
563
  help="Choose which RDKit Atom-level properties to extract.",
580
564
  )
581
565
 
@@ -673,23 +657,17 @@ def graph_representation(
673
657
 
674
658
  st.markdown("#### Bond Properties")
675
659
 
676
- select_all_bond = st.toggle("Select all bond properties")
677
-
678
- if select_all_bond:
679
- default_bond_properties = bond_properties
680
-
681
- else:
682
- default_bond_properties = [
683
- BondFeature.GetBondTypeAsDouble,
684
- BondFeature.GetIsAromatic,
685
- BondFeature.GetStereo,
686
- ]
687
-
688
- selected_bond_properties = st.multiselect(
660
+ selected_bond_properties = multiselect_with_select_all(
689
661
  "Select bond properties to compute:",
690
662
  options=bond_properties,
691
- default=default_bond_properties,
692
663
  key=DescriptorCalculationStateKeys.BondProperties,
664
+ select_all_label="Select all bond properties",
665
+ select_all_key=DescriptorCalculationStateKeys.SelectAllBondProperties,
666
+ partial_selection=[
667
+ BondFeature.GetBondTypeAsDouble,
668
+ BondFeature.GetIsAromatic,
669
+ BondFeature.GetStereo,
670
+ ],
693
671
  help="Choose which RDKit Bond-level properties to extract.",
694
672
  )
695
673
 
@@ -0,0 +1,73 @@
1
+ """
2
+ polynet.app.components.select_all
3
+ =================================
4
+ A multiselect with a "select all" toggle or checkbox.
5
+
6
+ Recent Streamlit versions keep a keyed widget's value when its ``default``
7
+ changes, so a toggle that only changes the multiselect's ``default`` does
8
+ nothing there. Here the toggle sets the selection itself, through the session
9
+ state, when it is clicked.
10
+ """
11
+
12
+ from collections.abc import Sequence
13
+
14
+ import streamlit as st
15
+
16
+
17
+ def multiselect_with_select_all(
18
+ label: str,
19
+ options: Sequence,
20
+ key: str,
21
+ *,
22
+ select_all_label: str,
23
+ select_all_key: str,
24
+ select_all_default: bool = False,
25
+ partial_selection: Sequence | None = None,
26
+ use_checkbox: bool = False,
27
+ select_all_help: str | None = None,
28
+ **multiselect_kwargs,
29
+ ) -> list:
30
+ """
31
+ Render a "select all" toggle (or checkbox) above a multiselect.
32
+
33
+ Switching it on selects every option; switching it off selects
34
+ ``partial_selection`` (nothing by default). The user can still edit the
35
+ selection afterwards.
36
+
37
+ Args:
38
+ label: Label of the multiselect.
39
+ options: The multiselect options.
40
+ key: Session-state key of the multiselect.
41
+ select_all_label: Label of the toggle.
42
+ select_all_key: Session-state key of the toggle.
43
+ select_all_default: Whether the toggle starts switched on.
44
+ partial_selection: Selection while the toggle is off.
45
+ use_checkbox: Show a checkbox instead of a toggle.
46
+ select_all_help: Help text of the toggle.
47
+ **multiselect_kwargs: Passed to ``st.multiselect`` (e.g. ``help``).
48
+
49
+ Returns:
50
+ The selected options.
51
+ """
52
+ options = list(options)
53
+ partial = [o for o in (partial_selection or []) if o in options]
54
+
55
+ def selection(select_all: bool) -> list:
56
+ return list(options) if select_all else list(partial)
57
+
58
+ def apply_select_all() -> None:
59
+ st.session_state[key] = selection(st.session_state[select_all_key])
60
+
61
+ (st.checkbox if use_checkbox else st.toggle)(
62
+ select_all_label,
63
+ value=select_all_default,
64
+ key=select_all_key,
65
+ help=select_all_help,
66
+ on_change=apply_select_all,
67
+ )
68
+ if key not in st.session_state:
69
+ st.session_state[key] = selection(st.session_state[select_all_key])
70
+ else:
71
+ # Options can change between runs (e.g. another experiment).
72
+ st.session_state[key] = [o for o in st.session_state[key] if o in options]
73
+ return st.multiselect(label, options=options, key=key, **multiselect_kwargs)
@@ -58,6 +58,8 @@ class DescriptorCalculationStateKeys(StrEnum):
58
58
  SelectAllPMXBackboneDescriptors = "SelectAllPMXBackboneDescriptors"
59
59
  AtomProperties = "AtomProperties"
60
60
  BondProperties = "BondProperties"
61
+ SelectAllAtomProperties = "SelectAllAtomProperties"
62
+ SelectAllBondProperties = "SelectAllBondProperties"
61
63
  ShowFrequency = "ShowFrequency"
62
64
 
63
65
 
@@ -531,7 +531,13 @@ def get_polybert_fingerprints(
531
531
 
532
532
  canonicalize = import_psmiles_canonicaliser()
533
533
 
534
- model = SentenceTransformer(POLYBERT_MODEL)
534
+ import torch
535
+
536
+ # Same device rule as GNN training (CUDA or CPU). sentence-transformers would
537
+ # otherwise use Apple's MPS, whose embeddings differ slightly from the CPU ones
538
+ # (so results would depend on the machine) and are NaN on some virtual Macs.
539
+ device = "cuda" if torch.cuda.is_available() else "cpu"
540
+ model = SentenceTransformer(POLYBERT_MODEL, device=device)
535
541
 
536
542
  # 1) Canonicalize (keep a mapping from original -> canonical)
537
543
  orig_to_canon: dict[str, str] = {}
@@ -9,6 +9,10 @@ dataset, once for regression and once for classification, in a temporary
9
9
  folder, and prints a PASS / FAIL table that can be pasted into a bug report.
10
10
  The regression run also explains the models and predicts on new data.
11
11
 
12
+ XGBoost is first trained in a separate process with PyTorch loaded: on macOS
13
+ two OpenMP runtimes in one process crash it, which would end the check before
14
+ its report. The pipeline runs train XGBoost only if that passes.
15
+
12
16
  Usage
13
17
  -----
14
18
  polynet check # about a minute on a laptop
@@ -25,6 +29,8 @@ import logging
25
29
  from pathlib import Path
26
30
  import platform
27
31
  import shutil
32
+ import subprocess
33
+ import sys
28
34
  import tempfile
29
35
  import time
30
36
  import warnings
@@ -108,11 +114,16 @@ def synthetic_copolymers(n_samples: int, problem_type: str, seed: int = 42) -> p
108
114
 
109
115
 
110
116
  def check_config(
111
- problem_type: str, data_path: Path, out_dir: Path, epochs: int, predict_path: Path | None
117
+ problem_type: str,
118
+ data_path: Path,
119
+ out_dir: Path,
120
+ epochs: int,
121
+ predict_path: Path | None,
122
+ xgboost: bool = True,
112
123
  ) -> dict:
113
124
  """
114
- Return a small but complete pipeline config: GCN and random forest on
115
- Morgan fingerprints, two repeated splits.
125
+ Return a small but complete pipeline config: GCN, random forest and
126
+ XGBoost on Morgan fingerprints, two repeated splits.
116
127
 
117
128
  Args:
118
129
  problem_type (str): ``"regression"`` or ``"classification"``.
@@ -121,6 +132,7 @@ def check_config(
121
132
  epochs (int): GNN training epochs.
122
133
  predict_path (Path | None): CSV to predict after training; when given,
123
134
  the models are also explained.
135
+ xgboost (bool): Also train XGBoost.
124
136
 
125
137
  Returns:
126
138
  dict: The config, as ``polynet run`` reads it from YAML.
@@ -175,7 +187,10 @@ def check_config(
175
187
  "feature_preprocessing": {"scaler": "standard_scaler"},
176
188
  "tml_models": {
177
189
  "train_tml": True,
178
- "selected_models": {"random_forest": {"n_estimators": 20, "max_depth": 3}},
190
+ "selected_models": {
191
+ "random_forest": {"n_estimators": 20, "max_depth": 3},
192
+ **({"xgboost": {"n_estimators": 20, "max_depth": 3}} if xgboost else {}),
193
+ },
179
194
  },
180
195
  "explainability": {"enabled": explain, "fragmentation": "brics", "top_n": 5},
181
196
  "tml_explainability": {"enabled": explain, "top_n": 5},
@@ -223,8 +238,47 @@ def _check_psmiles() -> CheckResult:
223
238
  return CheckResult("PSMILES canonicaliser", "PASS", "installed and working")
224
239
 
225
240
 
241
+ # Trains XGBoost the way the pipeline does: after PyTorch has been imported.
242
+ _XGBOOST_WITH_TORCH = """
243
+ import numpy as np
244
+ import torch # noqa: F401
245
+ import xgboost
246
+ X = np.random.default_rng(0).normal(size=(200, 10))
247
+ xgboost.XGBRegressor(n_estimators=10).fit(X, X[:, 0])
248
+ """
249
+
250
+
251
+ def _check_xgboost_with_torch() -> CheckResult:
252
+ """Train XGBoost with PyTorch loaded, in a separate process that may crash."""
253
+ name = "XGBoost with PyTorch loaded"
254
+ try:
255
+ run = subprocess.run(
256
+ [sys.executable, "-c", _XGBOOST_WITH_TORCH], capture_output=True, text=True, timeout=300
257
+ )
258
+ except subprocess.TimeoutExpired:
259
+ return CheckResult(name, "FAIL", "did not finish within 5 minutes")
260
+ if run.returncode == 0:
261
+ return CheckResult(name, "PASS")
262
+ if run.returncode < 0: # killed by a signal, e.g. a segmentation fault
263
+ detail = f"crashed (signal {-run.returncode})"
264
+ if platform.system() == "Darwin":
265
+ detail += (
266
+ "; on macOS this is usually two OpenMP runtimes (PyTorch's and XGBoost's). "
267
+ 'Install a compatible PyTorch: pip install "torch<2.11"'
268
+ )
269
+ else:
270
+ detail = (run.stderr.strip().splitlines() or [f"exit code {run.returncode}"])[-1]
271
+ return CheckResult(name, "FAIL", detail + ". XGBoost is left out of the runs below.")
272
+
273
+
226
274
  def _check_pipeline(
227
- name: str, problem_type: str, work_dir: Path, epochs: int, n_samples: int, predict: bool
275
+ name: str,
276
+ problem_type: str,
277
+ work_dir: Path,
278
+ epochs: int,
279
+ n_samples: int,
280
+ predict: bool,
281
+ xgboost: bool = True,
228
282
  ) -> CheckResult:
229
283
  from polynet.pipeline.runner import main as run_pipeline
230
284
 
@@ -243,7 +297,9 @@ def _check_pipeline(
243
297
  out_dir = run_dir / "results"
244
298
  config_path = run_dir / "config.yaml"
245
299
  with open(config_path, "w") as f:
246
- yaml.safe_dump(check_config(problem_type, data_path, out_dir, epochs, predict_path), f)
300
+ yaml.safe_dump(
301
+ check_config(problem_type, data_path, out_dir, epochs, predict_path, xgboost), f
302
+ )
247
303
 
248
304
  collector = _ErrorCollector()
249
305
  polynet_logger = logging.getLogger("polynet")
@@ -325,7 +381,8 @@ def main(argv: list[str] | None = None) -> int:
325
381
 
326
382
  results = [_check_packages()]
327
383
  if results[0].status == "PASS":
328
- results += [_check_device(), _check_psmiles()]
384
+ results += [_check_device(), _check_psmiles(), _check_xgboost_with_torch()]
385
+ xgboost = results[-1].status == "PASS"
329
386
 
330
387
  work_dir = Path(tempfile.mkdtemp(prefix="polynet_check_"))
331
388
  pipeline_checks = [
@@ -335,7 +392,9 @@ def main(argv: list[str] | None = None) -> int:
335
392
  for name, problem_type, predict in pipeline_checks:
336
393
  print(f" running {name.lower()}…")
337
394
  results.append(
338
- _check_pipeline(name, problem_type, work_dir, args.epochs, args.samples, predict)
395
+ _check_pipeline(
396
+ name, problem_type, work_dir, args.epochs, args.samples, predict, xgboost
397
+ )
339
398
  )
340
399
 
341
400
  failed = any(r.status == "FAIL" for r in results)
@@ -458,7 +458,7 @@ def plot_bootstrap_boxplots(
458
458
  fig, ax = plt.subplots(figsize=(height, width), dpi=dpi)
459
459
  ax.boxplot(
460
460
  data,
461
- labels=model_names,
461
+ tick_labels=model_names, # `labels` was removed in Matplotlib 3.11
462
462
  patch_artist=True,
463
463
  boxprops=dict(facecolor=fill_colour, color=border_colour),
464
464
  medianprops=dict(color=median_colour, linewidth=2),
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "polynet"
3
- version = "1.0.2"
3
+ version = "1.0.3"
4
4
  description = "A Python package for polymer informatics: property prediction with graph neural networks and traditional machine learning"
5
5
  authors = [
6
6
  { name = "Eduardo Aguilar-Bejarano", email = "eduardo.aguilar-bejarano@nottingham.ac.uk" },
@@ -19,7 +19,10 @@ dependencies = [
19
19
  "pandas>=2.2.3,<3.0.0",
20
20
  "rdkit>=2025.3.2,<2026.0.0",
21
21
  "torch==2.2.0 ; sys_platform == 'darwin' and platform_machine == 'x86_64'",
22
- "torch>=2.5.1,<3.0.0 ; sys_platform != 'darwin' or platform_machine != 'x86_64'",
22
+ # Apple silicon: from torch 2.11 the bundled libomp is no longer named like Homebrew's,
23
+ # so XGBoost loads a second OpenMP runtime and crashes once torch is imported.
24
+ "torch>=2.5.1,<2.11 ; sys_platform == 'darwin' and platform_machine == 'arm64'",
25
+ "torch>=2.5.1,<3.0.0 ; sys_platform != 'darwin'",
23
26
  "torch-geometric==2.7",
24
27
  "scikit-learn>=1.6.1,<2.0.0",
25
28
  "matplotlib>=3.10.3,<4.0.0",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes