chemicalml 0.2.0 → 0.2.1

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 (304) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +7 -1
  3. data/CLAUDE.md +48 -41
  4. data/TODO.align/36-molecular-convention-coverage.md +46 -0
  5. data/TODO.align/37-compchem-convention-coverage.md +55 -0
  6. data/TODO.align/38-dictionary-convention-coverage.md +34 -0
  7. data/TODO.align/39-unit-dictionary-convention-coverage.md +33 -0
  8. data/TODO.align/40-unit-type-dictionary-convention-coverage.md +28 -0
  9. data/TODO.align/41-expand-canonical-model-layer.md +43 -0
  10. data/TODO.align/42-translator-wire-new-models.md +31 -0
  11. data/TODO.align/43-dictionary-model-no-to-h.md +39 -0
  12. data/TODO.align/44-schema3-only-and-schema24-module.md +44 -0
  13. data/TODO.align/45-documentation-and-final-verification.md +23 -0
  14. data/TODO.align/46-lattice-vector-content.md +24 -0
  15. data/TODO.align/47-molecule-crystal-spectra-property-lists.md +23 -0
  16. data/TODO.align/48-model-list-and-module-lists.md +21 -0
  17. data/TODO.align/49-more-chemistry-models.md +34 -0
  18. data/TODO.align/50-validation-report.md +29 -0
  19. data/TODO.align/51-convention-auto-detection.md +30 -0
  20. data/TODO.align/52-schema24-legacy-elements.md +28 -0
  21. data/TODO.align/53-round-trip-integration-specs.md +30 -0
  22. data/TODO.align/54-document-versioned-parser-shim.md +38 -0
  23. data/TODO.align/55-molecule-missing-attributes.md +20 -0
  24. data/TODO.align/56-geometry-models.md +24 -0
  25. data/TODO.align/57-compchem-models.md +26 -0
  26. data/TODO.align/58-remaining-models.md +24 -0
  27. data/TODO.align/59-wire-new-models-translator.md +18 -0
  28. data/TODO.align/60-docs-and-final-verification.md +19 -0
  29. data/lib/chemicalml/cml/base/abundance.rb +6 -0
  30. data/lib/chemicalml/cml/base/action.rb +20 -0
  31. data/lib/chemicalml/cml/base/action_list.rb +18 -0
  32. data/lib/chemicalml/cml/base/alternative.rb +28 -0
  33. data/lib/chemicalml/cml/base/amount.rb +2 -0
  34. data/lib/chemicalml/cml/base/angle.rb +12 -0
  35. data/lib/chemicalml/cml/base/annotation.rb +30 -0
  36. data/lib/chemicalml/cml/base/appinfo.rb +32 -0
  37. data/lib/chemicalml/cml/base/arg.rb +46 -0
  38. data/lib/chemicalml/cml/base/array.rb +22 -0
  39. data/lib/chemicalml/cml/base/array_list.rb +2 -0
  40. data/lib/chemicalml/cml/base/atom.rb +18 -0
  41. data/lib/chemicalml/cml/base/atom_array.rb +38 -0
  42. data/lib/chemicalml/cml/base/atom_parity.rb +8 -0
  43. data/lib/chemicalml/cml/base/atom_set.rb +2 -0
  44. data/lib/chemicalml/cml/base/atom_type.rb +4 -0
  45. data/lib/chemicalml/cml/base/atom_type_list.rb +2 -0
  46. data/lib/chemicalml/cml/base/atomic_basis_function.rb +4 -0
  47. data/lib/chemicalml/cml/base/band.rb +2 -0
  48. data/lib/chemicalml/cml/base/basis_set.rb +4 -0
  49. data/lib/chemicalml/cml/base/bond.rb +6 -0
  50. data/lib/chemicalml/cml/base/bond_array.rb +16 -0
  51. data/lib/chemicalml/cml/base/bond_set.rb +2 -0
  52. data/lib/chemicalml/cml/base/bond_stereo.rb +10 -0
  53. data/lib/chemicalml/cml/base/bond_type.rb +2 -0
  54. data/lib/chemicalml/cml/base/bond_type_list.rb +2 -0
  55. data/lib/chemicalml/cml/base/cell_parameter.rb +4 -0
  56. data/lib/chemicalml/cml/base/complex_object.rb +30 -0
  57. data/lib/chemicalml/cml/base/condition_list.rb +4 -0
  58. data/lib/chemicalml/cml/base/crystal.rb +2 -0
  59. data/lib/chemicalml/cml/base/description.rb +2 -0
  60. data/lib/chemicalml/cml/base/dictionary.rb +6 -0
  61. data/lib/chemicalml/cml/base/dimension.rb +10 -0
  62. data/lib/chemicalml/cml/base/eigen.rb +2 -0
  63. data/lib/chemicalml/cml/base/electron.rb +6 -0
  64. data/lib/chemicalml/cml/base/enumeration.rb +32 -0
  65. data/lib/chemicalml/cml/base/expression.rb +32 -0
  66. data/lib/chemicalml/cml/base/float.rb +40 -0
  67. data/lib/chemicalml/cml/base/float_array.rb +42 -0
  68. data/lib/chemicalml/cml/base/fragment.rb +6 -0
  69. data/lib/chemicalml/cml/base/fragment_list.rb +4 -0
  70. data/lib/chemicalml/cml/base/identifier.rb +8 -0
  71. data/lib/chemicalml/cml/base/integer.rb +40 -0
  72. data/lib/chemicalml/cml/base/integer_array.rb +42 -0
  73. data/lib/chemicalml/cml/base/isotope.rb +2 -0
  74. data/lib/chemicalml/cml/base/isotope_list.rb +2 -0
  75. data/lib/chemicalml/cml/base/join.rb +6 -0
  76. data/lib/chemicalml/cml/base/kpoint.rb +2 -0
  77. data/lib/chemicalml/cml/base/label.rb +2 -0
  78. data/lib/chemicalml/cml/base/lattice.rb +2 -0
  79. data/lib/chemicalml/cml/base/lattice_vector.rb +2 -0
  80. data/lib/chemicalml/cml/base/length.rb +10 -0
  81. data/lib/chemicalml/cml/base/line3.rb +6 -0
  82. data/lib/chemicalml/cml/base/link.rb +20 -0
  83. data/lib/chemicalml/cml/base/list.rb +2 -0
  84. data/lib/chemicalml/cml/base/map.rb +12 -0
  85. data/lib/chemicalml/cml/base/matrix.rb +12 -0
  86. data/lib/chemicalml/cml/base/metadata.rb +4 -0
  87. data/lib/chemicalml/cml/base/metadata_list.rb +6 -0
  88. data/lib/chemicalml/cml/base/molecule.rb +10 -0
  89. data/lib/chemicalml/cml/base/molecule_list.rb +2 -0
  90. data/lib/chemicalml/cml/base/name.rb +2 -0
  91. data/lib/chemicalml/cml/base/object.rb +4 -0
  92. data/lib/chemicalml/cml/base/observation.rb +4 -0
  93. data/lib/chemicalml/cml/base/operator.rb +32 -0
  94. data/lib/chemicalml/cml/base/parameter.rb +10 -0
  95. data/lib/chemicalml/cml/base/parameter_list.rb +6 -0
  96. data/lib/chemicalml/cml/base/particle.rb +6 -0
  97. data/lib/chemicalml/cml/base/peak.rb +30 -0
  98. data/lib/chemicalml/cml/base/peak_group.rb +38 -0
  99. data/lib/chemicalml/cml/base/peak_list.rb +2 -0
  100. data/lib/chemicalml/cml/base/peak_structure.rb +14 -0
  101. data/lib/chemicalml/cml/base/plane3.rb +2 -0
  102. data/lib/chemicalml/cml/base/point3.rb +2 -0
  103. data/lib/chemicalml/cml/base/potential_form.rb +2 -0
  104. data/lib/chemicalml/cml/base/product.rb +16 -0
  105. data/lib/chemicalml/cml/base/product_list.rb +14 -0
  106. data/lib/chemicalml/cml/base/property.rb +6 -0
  107. data/lib/chemicalml/cml/base/property_list.rb +6 -0
  108. data/lib/chemicalml/cml/base/reactant.rb +16 -0
  109. data/lib/chemicalml/cml/base/reactant_list.rb +14 -0
  110. data/lib/chemicalml/cml/base/reaction.rb +16 -0
  111. data/lib/chemicalml/cml/base/reaction_list.rb +12 -0
  112. data/lib/chemicalml/cml/base/reaction_scheme.rb +10 -0
  113. data/lib/chemicalml/cml/base/reaction_step.rb +6 -0
  114. data/lib/chemicalml/cml/base/reaction_step_list.rb +8 -0
  115. data/lib/chemicalml/cml/base/region.rb +8 -0
  116. data/lib/chemicalml/cml/base/related_entry.rb +26 -0
  117. data/lib/chemicalml/cml/base/sample.rb +4 -0
  118. data/lib/chemicalml/cml/base/scalar.rb +18 -0
  119. data/lib/chemicalml/cml/base/spectator.rb +2 -0
  120. data/lib/chemicalml/cml/base/spectrum.rb +12 -0
  121. data/lib/chemicalml/cml/base/spectrum_data.rb +2 -0
  122. data/lib/chemicalml/cml/base/spectrum_list.rb +4 -0
  123. data/lib/chemicalml/cml/base/sphere3.rb +2 -0
  124. data/lib/chemicalml/cml/base/string.rb +32 -0
  125. data/lib/chemicalml/cml/base/string_array.rb +40 -0
  126. data/lib/chemicalml/cml/base/substance.rb +12 -0
  127. data/lib/chemicalml/cml/base/substance_list.rb +6 -0
  128. data/lib/chemicalml/cml/base/symmetry.rb +4 -0
  129. data/lib/chemicalml/cml/base/system.rb +4 -0
  130. data/lib/chemicalml/cml/base/table.rb +10 -0
  131. data/lib/chemicalml/cml/base/table_content.rb +2 -0
  132. data/lib/chemicalml/cml/base/table_header_cell.rb +10 -0
  133. data/lib/chemicalml/cml/base/tcell.rb +34 -0
  134. data/lib/chemicalml/cml/base/torsion.rb +12 -0
  135. data/lib/chemicalml/cml/base/trow.rb +32 -0
  136. data/lib/chemicalml/cml/base/unit.rb +12 -0
  137. data/lib/chemicalml/cml/base/unit_list.rb +12 -0
  138. data/lib/chemicalml/cml/base/unit_type.rb +10 -0
  139. data/lib/chemicalml/cml/base/unit_type_list.rb +10 -0
  140. data/lib/chemicalml/cml/base/vector3.rb +2 -0
  141. data/lib/chemicalml/cml/base/xaxis.rb +6 -0
  142. data/lib/chemicalml/cml/base/yaxis.rb +6 -0
  143. data/lib/chemicalml/cml/base.rb +17 -0
  144. data/lib/chemicalml/cml/elements.rb +39 -4
  145. data/lib/chemicalml/cml/role/alternative.rb +10 -0
  146. data/lib/chemicalml/cml/role/annotation.rb +10 -0
  147. data/lib/chemicalml/cml/role/appinfo.rb +10 -0
  148. data/lib/chemicalml/cml/role/arg.rb +10 -0
  149. data/lib/chemicalml/cml/role/cml_module.rb +2 -3
  150. data/lib/chemicalml/cml/role/complex_object.rb +10 -0
  151. data/lib/chemicalml/cml/role/enumeration.rb +10 -0
  152. data/lib/chemicalml/cml/role/expression.rb +10 -0
  153. data/lib/chemicalml/cml/role/float.rb +10 -0
  154. data/lib/chemicalml/cml/role/float_array.rb +10 -0
  155. data/lib/chemicalml/cml/role/integer.rb +10 -0
  156. data/lib/chemicalml/cml/role/integer_array.rb +10 -0
  157. data/lib/chemicalml/cml/role/operator.rb +10 -0
  158. data/lib/chemicalml/cml/role/related_entry.rb +10 -0
  159. data/lib/chemicalml/cml/role/string.rb +10 -0
  160. data/lib/chemicalml/cml/role/string_array.rb +10 -0
  161. data/lib/chemicalml/cml/role/tcell.rb +10 -0
  162. data/lib/chemicalml/cml/role/trow.rb +10 -0
  163. data/lib/chemicalml/cml/role.rb +17 -0
  164. data/lib/chemicalml/cml/schema24/alternative.rb +14 -0
  165. data/lib/chemicalml/cml/schema24/annotation.rb +15 -0
  166. data/lib/chemicalml/cml/schema24/appinfo.rb +15 -0
  167. data/lib/chemicalml/cml/schema24/arg.rb +14 -0
  168. data/lib/chemicalml/cml/schema24/cml_module.rb +18 -0
  169. data/lib/chemicalml/cml/schema24/complex_object.rb +14 -0
  170. data/lib/chemicalml/cml/schema24/configuration.rb +5 -2
  171. data/lib/chemicalml/cml/schema24/enumeration.rb +16 -0
  172. data/lib/chemicalml/cml/schema24/expression.rb +14 -0
  173. data/lib/chemicalml/cml/schema24/float.rb +14 -0
  174. data/lib/chemicalml/cml/schema24/float_array.rb +14 -0
  175. data/lib/chemicalml/cml/schema24/integer.rb +14 -0
  176. data/lib/chemicalml/cml/schema24/integer_array.rb +14 -0
  177. data/lib/chemicalml/cml/schema24/operator.rb +14 -0
  178. data/lib/chemicalml/cml/schema24/related_entry.rb +14 -0
  179. data/lib/chemicalml/cml/schema24/string.rb +14 -0
  180. data/lib/chemicalml/cml/schema24/string_array.rb +14 -0
  181. data/lib/chemicalml/cml/schema24/tcell.rb +14 -0
  182. data/lib/chemicalml/cml/schema24/trow.rb +14 -0
  183. data/lib/chemicalml/cml/schema24.rb +33 -5
  184. data/lib/chemicalml/cml.rb +1 -3
  185. data/lib/chemicalml/context_configuration.rb +8 -2
  186. data/lib/chemicalml/convention/base.rb +17 -3
  187. data/lib/chemicalml/convention/compchem/constraints/array_rules.rb +44 -0
  188. data/lib/chemicalml/convention/compchem/constraints/calculation_requires_finalization.rb +28 -0
  189. data/lib/chemicalml/convention/compchem/constraints/compchem_module_must_contain_job_list.rb +4 -4
  190. data/lib/chemicalml/convention/compchem/constraints/environment_at_most_one_property_list.rb +26 -0
  191. data/lib/chemicalml/convention/compchem/constraints/environment_must_not_contain_parameter.rb +28 -0
  192. data/lib/chemicalml/convention/compchem/constraints/finalization_at_most_one_molecule.rb +26 -0
  193. data/lib/chemicalml/convention/compchem/constraints/finalization_at_most_one_property_list.rb +26 -0
  194. data/lib/chemicalml/convention/compchem/constraints/finalization_must_not_contain_parameter.rb +27 -0
  195. data/lib/chemicalml/convention/compchem/constraints/initialization_at_most_one_molecule.rb +26 -0
  196. data/lib/chemicalml/convention/compchem/constraints/initialization_at_most_one_parameter_list.rb +26 -0
  197. data/lib/chemicalml/convention/compchem/constraints/initialization_must_not_contain_property.rb +28 -0
  198. data/lib/chemicalml/convention/compchem/constraints/job_list_module_must_have_id.rb +24 -0
  199. data/lib/chemicalml/convention/compchem/constraints/job_module_at_most_one_environment.rb +25 -0
  200. data/lib/chemicalml/convention/compchem/constraints/job_module_at_most_one_finalization.rb +25 -0
  201. data/lib/chemicalml/convention/compchem/constraints/job_module_must_have_id.rb +24 -0
  202. data/lib/chemicalml/convention/compchem/constraints/job_must_contain_initialization.rb +6 -9
  203. data/lib/chemicalml/convention/compchem/constraints/matrix_rules.rb +48 -0
  204. data/lib/chemicalml/convention/compchem/constraints/module_predicates.rb +50 -0
  205. data/lib/chemicalml/convention/compchem/constraints/scalar_units.rb +42 -0
  206. data/lib/chemicalml/convention/compchem/constraints.rb +36 -2
  207. data/lib/chemicalml/convention/compchem.rb +16 -0
  208. data/lib/chemicalml/convention/constraint.rb +31 -18
  209. data/lib/chemicalml/convention/coordinator.rb +89 -0
  210. data/lib/chemicalml/convention/detection.rb +33 -0
  211. data/lib/chemicalml/convention/dictionary/constraints/dictionary_must_have_namespace.rb +28 -0
  212. data/lib/chemicalml/convention/dictionary/constraints/dictionary_namespace_should_end_with_slash_or_hash.rb +32 -0
  213. data/lib/chemicalml/convention/dictionary/constraints/entry_id_must_match_pattern.rb +33 -0
  214. data/lib/chemicalml/convention/dictionary/constraints/entry_ids_unique_within_dictionary.rb +3 -3
  215. data/lib/chemicalml/convention/dictionary/constraints/entry_must_contain_definition.rb +28 -0
  216. data/lib/chemicalml/convention/dictionary/constraints/entry_must_have_id_and_term.rb +5 -5
  217. data/lib/chemicalml/convention/dictionary/constraints/entry_must_have_unit_type.rb +30 -0
  218. data/lib/chemicalml/convention/dictionary/constraints/entry_units_co_constraints.rb +44 -0
  219. data/lib/chemicalml/convention/dictionary/constraints.rb +12 -0
  220. data/lib/chemicalml/convention/dictionary.rb +6 -0
  221. data/lib/chemicalml/convention/molecular/constraints/atom_array_must_be_child_of_molecule_or_formula.rb +40 -0
  222. data/lib/chemicalml/convention/molecular/constraints/atom_array_must_contain_atoms.rb +3 -3
  223. data/lib/chemicalml/convention/molecular/constraints/atom_coordinates_must_be_paired.rb +5 -5
  224. data/lib/chemicalml/convention/molecular/constraints/atom_id_must_match_pattern.rb +4 -4
  225. data/lib/chemicalml/convention/molecular/constraints/atom_ids_unique_within_molecule.rb +2 -2
  226. data/lib/chemicalml/convention/molecular/constraints/atom_must_have_element_type.rb +3 -3
  227. data/lib/chemicalml/convention/molecular/constraints/atom_must_have_id.rb +3 -3
  228. data/lib/chemicalml/convention/molecular/constraints/bond_array_must_be_child_of_molecule.rb +36 -0
  229. data/lib/chemicalml/convention/molecular/constraints/bond_ids_unique_within_molecule.rb +43 -0
  230. data/lib/chemicalml/convention/molecular/constraints/bond_must_have_atom_refs2.rb +3 -3
  231. data/lib/chemicalml/convention/molecular/constraints/bond_must_have_order.rb +3 -3
  232. data/lib/chemicalml/convention/molecular/constraints/bond_must_reference_atoms_in_same_molecule.rb +3 -3
  233. data/lib/chemicalml/convention/molecular/constraints/bond_order_other_must_have_dict_ref.rb +33 -0
  234. data/lib/chemicalml/convention/molecular/constraints/bond_order_should_not_be_numeric.rb +2 -2
  235. data/lib/chemicalml/convention/molecular/constraints/bond_stereo_cis_trans_must_have_atom_refs4.rb +45 -0
  236. data/lib/chemicalml/convention/molecular/constraints/bond_stereo_other_must_have_dict_ref.rb +32 -0
  237. data/lib/chemicalml/convention/molecular/constraints/bond_stereo_wedge_hash_must_have_atom_refs2.rb +45 -0
  238. data/lib/chemicalml/convention/molecular/constraints/molecule_atom_array_mutually_exclusive_with_children.rb +32 -0
  239. data/lib/chemicalml/convention/molecular/constraints/molecule_bond_array_mutually_exclusive_with_children.rb +31 -0
  240. data/lib/chemicalml/convention/molecular/constraints/molecule_count_must_not_appear_on_top_level.rb +34 -0
  241. data/lib/chemicalml/convention/molecular/constraints/molecule_must_have_id.rb +3 -3
  242. data/lib/chemicalml/convention/molecular/constraints/property_must_have_dict_ref.rb +3 -3
  243. data/lib/chemicalml/convention/molecular/constraints/scalar_must_have_data_type.rb +3 -3
  244. data/lib/chemicalml/convention/molecular/constraints.rb +20 -0
  245. data/lib/chemicalml/convention/molecular.rb +10 -0
  246. data/lib/chemicalml/convention/registry.rb +17 -0
  247. data/lib/chemicalml/convention/unit_dictionary/constraints/unit_list_must_contain_at_least_one_unit.rb +28 -0
  248. data/lib/chemicalml/convention/unit_dictionary/constraints/unit_list_must_have_namespace.rb +36 -0
  249. data/lib/chemicalml/convention/unit_dictionary/constraints/unit_must_contain_definition.rb +27 -0
  250. data/lib/chemicalml/convention/unit_dictionary/constraints/unit_must_have_id.rb +27 -0
  251. data/lib/chemicalml/convention/unit_dictionary/constraints/unit_must_have_symbol_and_unit_type.rb +3 -3
  252. data/lib/chemicalml/convention/unit_dictionary/constraints.rb +9 -1
  253. data/lib/chemicalml/convention/unit_dictionary.rb +4 -0
  254. data/lib/chemicalml/convention/unit_type_dictionary/constraints/unit_type_id_must_match_pattern.rb +31 -0
  255. data/lib/chemicalml/convention/unit_type_dictionary/constraints/unit_type_list_must_contain_at_least_one_unit_type.rb +27 -0
  256. data/lib/chemicalml/convention/unit_type_dictionary/constraints/unit_type_list_must_have_namespace.rb +35 -0
  257. data/lib/chemicalml/convention/unit_type_dictionary/constraints/unit_type_must_contain_definition.rb +27 -0
  258. data/lib/chemicalml/convention/unit_type_dictionary/constraints/unit_type_must_have_id_and_name.rb +5 -5
  259. data/lib/chemicalml/convention/unit_type_dictionary/constraints.rb +9 -1
  260. data/lib/chemicalml/convention/unit_type_dictionary.rb +4 -0
  261. data/lib/chemicalml/convention/validation_report.rb +63 -0
  262. data/lib/chemicalml/convention.rb +11 -0
  263. data/lib/chemicalml/dictionary/entry.rb +6 -6
  264. data/lib/chemicalml/dictionary/enum.rb +1 -1
  265. data/lib/chemicalml/dictionary/link.rb +3 -3
  266. data/lib/chemicalml/dictionary/loader.rb +19 -19
  267. data/lib/chemicalml/dictionary/model.rb +5 -5
  268. data/lib/chemicalml/dictionary/registry.rb +9 -9
  269. data/lib/chemicalml/version.rb +1 -1
  270. data/lib/chemicalml/versioned_parser.rb +18 -0
  271. data/lib/chemicalml.rb +1 -2
  272. metadata +123 -34
  273. data/lib/chemicalml/cml/child_mappings.rb +0 -168
  274. data/lib/chemicalml/cml/translator/value_translations.rb +0 -270
  275. data/lib/chemicalml/cml/translator.rb +0 -484
  276. data/lib/chemicalml/model/atom.rb +0 -64
  277. data/lib/chemicalml/model/atom_parity.rb +0 -21
  278. data/lib/chemicalml/model/bond.rb +0 -39
  279. data/lib/chemicalml/model/bond_stereo.rb +0 -29
  280. data/lib/chemicalml/model/cml_array.rb +0 -27
  281. data/lib/chemicalml/model/cml_module.rb +0 -45
  282. data/lib/chemicalml/model/document.rb +0 -29
  283. data/lib/chemicalml/model/formula.rb +0 -34
  284. data/lib/chemicalml/model/identifier.rb +0 -21
  285. data/lib/chemicalml/model/label.rb +0 -21
  286. data/lib/chemicalml/model/matrix.rb +0 -29
  287. data/lib/chemicalml/model/metadata.rb +0 -23
  288. data/lib/chemicalml/model/metadata_list.rb +0 -25
  289. data/lib/chemicalml/model/molecule.rb +0 -45
  290. data/lib/chemicalml/model/name.rb +0 -21
  291. data/lib/chemicalml/model/node.rb +0 -45
  292. data/lib/chemicalml/model/parameter.rb +0 -27
  293. data/lib/chemicalml/model/parameter_list.rb +0 -25
  294. data/lib/chemicalml/model/product.rb +0 -21
  295. data/lib/chemicalml/model/product_list.rb +0 -21
  296. data/lib/chemicalml/model/property.rb +0 -28
  297. data/lib/chemicalml/model/property_list.rb +0 -25
  298. data/lib/chemicalml/model/reactant.rb +0 -21
  299. data/lib/chemicalml/model/reactant_list.rb +0 -21
  300. data/lib/chemicalml/model/reaction.rb +0 -41
  301. data/lib/chemicalml/model/reaction_list.rb +0 -23
  302. data/lib/chemicalml/model/scalar.rb +0 -25
  303. data/lib/chemicalml/model/substance.rb +0 -25
  304. data/lib/chemicalml/model.rb +0 -40
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chemicalml
4
+ module Convention
5
+ # Single-pass constraint dispatcher. Replaces the previous
6
+ # O(N*C) walk (every constraint walked the whole tree) with
7
+ # O(N + total_applicable) — one tree walk per validation, with
8
+ # each node dispatched only to constraints that declared
9
+ # `applies_to` matching one of the node's ancestors.
10
+ #
11
+ # Constraints that don't declare `applies_to` fall into a
12
+ # default "all nodes" bucket and run for every visited node,
13
+ # preserving backward compatibility.
14
+ class Coordinator
15
+ attr_reader :document_constraints, :node_constraints
16
+
17
+ def initialize(constraint_classes)
18
+ @document_constraints = constraint_classes
19
+ .select { |c| c < Constraint::DocumentConstraint }
20
+ .map(&:new)
21
+ @node_constraints = constraint_constraints(constraint_classes)
22
+ .map(&:new)
23
+ @always_applicable = []
24
+ @by_role = Hash.new { |h, k| h[k] = [] }
25
+ @node_constraints.each { |c| register(c) }
26
+ end
27
+
28
+ def validate(document)
29
+ violations = @document_constraints.flat_map do |constraint|
30
+ Array(constraint.check(document))
31
+ end
32
+ return violations unless visitable?(document)
33
+
34
+ walk(document) do |node, path|
35
+ applicable_for(node).each do |constraint|
36
+ violations.concat(Array(constraint.check_node(node, path)))
37
+ end
38
+ end
39
+ violations
40
+ end
41
+
42
+ private
43
+
44
+ def constraint_constraints(classes)
45
+ classes.select { |c| c < Constraint::NodeConstraint }
46
+ end
47
+
48
+ def register(constraint)
49
+ roles = constraint.class.applies_to_roles
50
+ if roles.nil? || roles.empty?
51
+ @always_applicable << constraint
52
+ else
53
+ roles.each { |role| @by_role[role] << constraint }
54
+ end
55
+ end
56
+
57
+ def visitable?(node)
58
+ node.is_a?(Chemicalml::Cml::Visitable)
59
+ end
60
+
61
+ def applicable_for(node)
62
+ applicable = @always_applicable
63
+ node.class.ancestors.each do |ancestor|
64
+ next unless ancestor.is_a?(::Module)
65
+ bucket = @by_role[ancestor]
66
+ next unless bucket
67
+
68
+ applicable += bucket
69
+ end
70
+ applicable
71
+ end
72
+
73
+ def walk(node, path = [], &block)
74
+ return unless node
75
+ return unless visitable?(node)
76
+
77
+ yield(node, path)
78
+ node.wire_children.each do |child|
79
+ walk(child, path + [describe(child)], &block)
80
+ end
81
+ end
82
+
83
+ def describe(node)
84
+ id_value = node.node_id
85
+ id_value ? "#{node.element_name}[#{id_value}]" : node.element_name
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chemicalml
4
+ module Convention
5
+ # Convention detection from a wire document. Reads the
6
+ # `convention` attribute from the root element (Document, Module,
7
+ # Molecule, Dictionary, UnitList, or UnitTypeList) and returns
8
+ # the QName string (e.g. "convention:molecular").
9
+ #
10
+ # Used by `Registry.detect_and_validate` and reusable by callers
11
+ # that want to know which convention a document declares without
12
+ # running validation.
13
+ module Detection
14
+ CONVENTION_ROOTS = [
15
+ Chemicalml::Cml::Role::Document,
16
+ Chemicalml::Cml::Role::Module,
17
+ Chemicalml::Cml::Role::Molecule,
18
+ Chemicalml::Cml::Role::Dictionary,
19
+ Chemicalml::Cml::Role::UnitList,
20
+ Chemicalml::Cml::Role::UnitTypeList
21
+ ].freeze
22
+
23
+ def self.convention_of(document)
24
+ CONVENTION_ROOTS.each do |role|
25
+ next unless document.is_a?(role)
26
+
27
+ return document.convention if document.convention
28
+ end
29
+ nil
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chemicalml
4
+ module Convention
5
+ module Dictionary
6
+ module Constraints
7
+ # A `<dictionary>` element MUST have a `namespace` attribute
8
+ # whose value is a valid URI defining the scope within which
9
+ # the entry terms are unique.
10
+ class DictionaryMustHaveNamespace < Chemicalml::Convention::Constraint::NodeConstraint
11
+ applies_to Chemicalml::Cml::Role::Dictionary
12
+ def check_node(node, path)
13
+ return [] unless node.namespace.to_s.empty?
14
+
15
+ [violation(path: path.join('/'),
16
+ message: 'dictionary must have a namespace attribute')]
17
+ end
18
+
19
+ private
20
+
21
+ def dictionary?(node)
22
+ node.is_a?(Chemicalml::Cml::Role::Dictionary)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chemicalml
4
+ module Convention
5
+ module Dictionary
6
+ module Constraints
7
+ # The `namespace` URI SHOULD end with `/` or `#` so terms can
8
+ # be referenced by appending them to the URI. Warning level.
9
+ class DictionaryNamespaceShouldEndWithSlashOrHash < Chemicalml::Convention::Constraint::NodeConstraint
10
+ applies_to Chemicalml::Cml::Role::Dictionary
11
+ def check_node(node, path)
12
+
13
+ ns = node.namespace.to_s
14
+ return [] if ns.empty?
15
+ return [] if ns.end_with?('/', '#')
16
+
17
+ [violation(path: path.join('/'),
18
+ message: "dictionary namespace #{ns.inspect} should end with / or # " \
19
+ 'so terms can be referenced by appending',
20
+ severity: :warning)]
21
+ end
22
+
23
+ private
24
+
25
+ def dictionary?(node)
26
+ node.is_a?(Chemicalml::Cml::Role::Dictionary)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chemicalml
4
+ module Convention
5
+ module Dictionary
6
+ module Constraints
7
+ # An `<entry>` `id` MUST match `[A-Za-z][A-Za-z0-9._-]*` —
8
+ # start with a letter, followed by letters/digits/dot/hyphen/
9
+ # underscore. Mirrors the upstream IdStartChar / IdChar BNF.
10
+ class EntryIdMustMatchPattern < Chemicalml::Convention::Constraint::NodeConstraint
11
+ applies_to Chemicalml::Cml::Role::DictionaryEntry
12
+ PATTERN = /\A[A-Za-z][A-Za-z0-9._-]*\z/
13
+
14
+ def check_node(node, path)
15
+
16
+ id = node.id.to_s
17
+ return [] if id.empty?
18
+ return [] if id.match?(PATTERN)
19
+
20
+ [violation(path: path.join('/'),
21
+ message: "entry id #{id.inspect} must match [A-Za-z][A-Za-z0-9._-]*")]
22
+ end
23
+
24
+ private
25
+
26
+ def entry?(node)
27
+ node.is_a?(Chemicalml::Cml::Role::DictionaryEntry)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -7,16 +7,16 @@ module Chemicalml
7
7
  # Entry ids MUST be unique within the parent dictionary per
8
8
  # the dictionary convention.
9
9
  class EntryIdsUniqueWithinDictionary < Chemicalml::Convention::Constraint::NodeConstraint
10
+ applies_to Chemicalml::Cml::Role::Dictionary
10
11
  def check_node(node, path)
11
- return [] unless dictionary?(node)
12
12
 
13
13
  entries = node.entries || []
14
- ids = entries.map { |e| e.id }.compact
14
+ ids = entries.map(&:id).compact
15
15
  duplicates = ids.group_by { |x| x }.select { |_, v| v.length > 1 }.keys
16
16
  return [] if duplicates.empty?
17
17
 
18
18
  duplicates.map do |dup|
19
- violation(path: path.join("/"),
19
+ violation(path: path.join('/'),
20
20
  message: "duplicate entry id #{dup.inspect} within dictionary")
21
21
  end
22
22
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chemicalml
4
+ module Convention
5
+ module Dictionary
6
+ module Constraints
7
+ # An `<entry>` MUST contain a single `definition` child
8
+ # element. The schema allows it as a string; this constraint
9
+ # enforces presence.
10
+ class EntryMustContainDefinition < Chemicalml::Convention::Constraint::NodeConstraint
11
+ applies_to Chemicalml::Cml::Role::DictionaryEntry
12
+ def check_node(node, path)
13
+ return [] unless node.definition.to_s.strip.empty?
14
+
15
+ [violation(path: path.join('/'),
16
+ message: "entry #{node.id.inspect} must contain a single definition child")]
17
+ end
18
+
19
+ private
20
+
21
+ def entry?(node)
22
+ node.is_a?(Chemicalml::Cml::Role::DictionaryEntry)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -7,17 +7,17 @@ module Chemicalml
7
7
  # Every entry MUST have an `id` and a `term` attribute per the
8
8
  # dictionary convention.
9
9
  class EntryMustHaveIdAndTerm < Chemicalml::Convention::Constraint::NodeConstraint
10
+ applies_to Chemicalml::Cml::Role::DictionaryEntry
10
11
  def check_node(node, path)
11
- return [] unless entry?(node)
12
12
 
13
13
  violations = []
14
14
  if node.id.to_s.empty?
15
- violations << violation(path: path.join("/"),
16
- message: "entry must have an id attribute")
15
+ violations << violation(path: path.join('/'),
16
+ message: 'entry must have an id attribute')
17
17
  end
18
18
  if node.term.to_s.empty?
19
- violations << violation(path: path.join("/"),
20
- message: "entry must have a term attribute")
19
+ violations << violation(path: path.join('/'),
20
+ message: 'entry must have a term attribute')
21
21
  end
22
22
  violations
23
23
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chemicalml
4
+ module Convention
5
+ module Dictionary
6
+ module Constraints
7
+ # An `<entry>` MUST have a `unitType` attribute. Entries
8
+ # describing concepts that should not have units (e.g. name
9
+ # of a program) should reference `none` in the standard CML
10
+ # unitType dictionary.
11
+ class EntryMustHaveUnitType < Chemicalml::Convention::Constraint::NodeConstraint
12
+ applies_to Chemicalml::Cml::Role::DictionaryEntry
13
+ def check_node(node, path)
14
+ return [] unless node.unit_type.to_s.empty?
15
+
16
+ [violation(path: path.join('/'),
17
+ message: "entry #{node.id.inspect} must have a unitType attribute " \
18
+ "(use 'none' for concepts without units)")]
19
+ end
20
+
21
+ private
22
+
23
+ def entry?(node)
24
+ node.is_a?(Chemicalml::Cml::Role::DictionaryEntry)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chemicalml
4
+ module Convention
5
+ module Dictionary
6
+ module Constraints
7
+ # Co-constraint between `unitType` and `units`:
8
+ #
9
+ # - If `unitType` is `unknown`, `units` MUST NOT be present.
10
+ # - If `unitType` is `none`, `units` MUST be present and point
11
+ # to `http://www.xml-cml.org/unit/si#none`.
12
+ class EntryUnitsCoConstraints < Chemicalml::Convention::Constraint::NodeConstraint
13
+ applies_to Chemicalml::Cml::Role::DictionaryEntry
14
+ NONE_UNITS = 'http://www.xml-cml.org/unit/si#none'
15
+
16
+ def check_node(node, path)
17
+
18
+ unit_type = node.unit_type.to_s
19
+ units = node.units.to_s
20
+ violations = []
21
+
22
+ if unit_type == 'unknown' && !units.empty?
23
+ violations << violation(path: path.join('/'),
24
+ message: "entry #{node.id.inspect} has unitType 'unknown' " \
25
+ 'and therefore must not have units')
26
+ end
27
+ if unit_type == 'none' && units != NONE_UNITS
28
+ violations << violation(path: path.join('/'),
29
+ message: "entry #{node.id.inspect} has unitType 'none' — " \
30
+ "units must be #{NONE_UNITS.inspect}")
31
+ end
32
+ violations
33
+ end
34
+
35
+ private
36
+
37
+ def entry?(node)
38
+ node.is_a?(Chemicalml::Cml::Role::DictionaryEntry)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -8,6 +8,18 @@ module Chemicalml
8
8
  "chemicalml/convention/dictionary/constraints/entry_must_have_id_and_term"
9
9
  autoload :EntryIdsUniqueWithinDictionary,
10
10
  "chemicalml/convention/dictionary/constraints/entry_ids_unique_within_dictionary"
11
+ autoload :DictionaryMustHaveNamespace,
12
+ "chemicalml/convention/dictionary/constraints/dictionary_must_have_namespace"
13
+ autoload :DictionaryNamespaceShouldEndWithSlashOrHash,
14
+ "chemicalml/convention/dictionary/constraints/dictionary_namespace_should_end_with_slash_or_hash"
15
+ autoload :EntryMustContainDefinition,
16
+ "chemicalml/convention/dictionary/constraints/entry_must_contain_definition"
17
+ autoload :EntryIdMustMatchPattern,
18
+ "chemicalml/convention/dictionary/constraints/entry_id_must_match_pattern"
19
+ autoload :EntryMustHaveUnitType,
20
+ "chemicalml/convention/dictionary/constraints/entry_must_have_unit_type"
21
+ autoload :EntryUnitsCoConstraints,
22
+ "chemicalml/convention/dictionary/constraints/entry_units_co_constraints"
11
23
  end
12
24
  end
13
25
  end
@@ -23,6 +23,12 @@ module Chemicalml
23
23
 
24
24
  register Constraints::EntryMustHaveIdAndTerm
25
25
  register Constraints::EntryIdsUniqueWithinDictionary
26
+ register Constraints::DictionaryMustHaveNamespace
27
+ register Constraints::DictionaryNamespaceShouldEndWithSlashOrHash
28
+ register Constraints::EntryMustContainDefinition
29
+ register Constraints::EntryIdMustMatchPattern
30
+ register Constraints::EntryMustHaveUnitType
31
+ register Constraints::EntryUnitsCoConstraints
26
32
  end
27
33
  end
28
34
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chemicalml
4
+ module Convention
5
+ module Molecular
6
+ module Constraints
7
+ # Molecular convention: `<atomArray>` MUST be a child of
8
+ # `<molecule>` or `<formula>`. Any other parent is invalid.
9
+ # Checked from the parent's perspective during the walk —
10
+ # when visiting a non-molecule/non-formula node, look for
11
+ # misplaced atomArray children.
12
+ class AtomArrayMustBeChildOfMoleculeOrFormula < Chemicalml::Convention::Constraint::NodeConstraint
13
+ def check_node(node, path)
14
+ return [] if allowed_parent?(node)
15
+
16
+ misplaced = node.wire_children.select { |c| atom_array?(c) }
17
+ misplaced.map do |child|
18
+ violation(
19
+ path: (path + [describe(node), describe(child)]).join('/'),
20
+ message: 'atomArray must be a child of molecule or formula, ' \
21
+ "but appears under #{node.element_name.inspect}"
22
+ )
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def allowed_parent?(node)
29
+ node.is_a?(Chemicalml::Cml::Role::Molecule) ||
30
+ node.is_a?(Chemicalml::Cml::Role::Formula)
31
+ end
32
+
33
+ def atom_array?(node)
34
+ node.is_a?(Chemicalml::Cml::Role::AtomArray)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -7,15 +7,15 @@ module Chemicalml
7
7
  # `atomArray` MUST contain at least one `atom` child per the
8
8
  # molecular convention.
9
9
  class AtomArrayMustContainAtoms < Chemicalml::Convention::Constraint::NodeConstraint
10
+ applies_to Chemicalml::Cml::Role::Molecule
10
11
  def check_node(node, path)
11
- return [] unless molecule?(node)
12
12
  return [] unless node.atom_array
13
13
 
14
14
  atoms = node.atom_array.atoms || []
15
15
  return [] unless atoms.empty?
16
16
 
17
- [violation(path: path.empty? ? "molecule" : path.join("/"),
18
- message: "atomArray must contain at least one atom")]
17
+ [violation(path: path.empty? ? 'molecule' : path.join('/'),
18
+ message: 'atomArray must contain at least one atom')]
19
19
  end
20
20
 
21
21
  private
@@ -5,23 +5,23 @@ module Chemicalml
5
5
  module Molecular
6
6
  module Constraints
7
7
  class AtomCoordinatesMustBePaired < Chemicalml::Convention::Constraint::NodeConstraint
8
+ applies_to Chemicalml::Cml::Role::Atom
8
9
  def check_node(node, path)
9
- return [] unless node.is_a?(Chemicalml::Cml::Role::Atom)
10
10
 
11
11
  violations = []
12
12
  has_x2 = !node.x2.to_s.empty?
13
13
  has_y2 = !node.y2.to_s.empty?
14
14
  if has_x2 != has_y2
15
- violations << violation(path: path.empty? ? "atom" : path.join("/"),
16
- message: "x2 and y2 must both be present or both absent")
15
+ violations << violation(path: path.empty? ? 'atom' : path.join('/'),
16
+ message: 'x2 and y2 must both be present or both absent')
17
17
  end
18
18
 
19
19
  has_x3 = !node.x3.to_s.empty?
20
20
  has_y3 = !node.y3.to_s.empty?
21
21
  has_z3 = !node.z3.to_s.empty?
22
22
  if [has_x3, has_y3, has_z3].any? && ![has_x3, has_y3, has_z3].all?
23
- violations << violation(path: path.empty? ? "atom" : path.join("/"),
24
- message: "x3, y3, and z3 must all be present together")
23
+ violations << violation(path: path.empty? ? 'atom' : path.join('/'),
24
+ message: 'x3, y3, and z3 must all be present together')
25
25
  end
26
26
  violations
27
27
  end
@@ -5,18 +5,18 @@ module Chemicalml
5
5
  module Molecular
6
6
  module Constraints
7
7
  class AtomIdMustMatchPattern < Chemicalml::Convention::Constraint::NodeConstraint
8
- ID_PATTERN = /\A[A-Za-z][A-Za-z0-9._\-]*\z/.freeze
8
+ applies_to Chemicalml::Cml::Role::Atom
9
+ ID_PATTERN = /\A[A-Za-z][A-Za-z0-9._-]*\z/
9
10
 
10
11
  def check_node(node, path)
11
- return [] unless node.is_a?(Chemicalml::Cml::Role::Atom)
12
12
 
13
13
  id = node.id.to_s
14
14
  return [] if id.empty?
15
15
  return [] if id.match?(ID_PATTERN)
16
16
 
17
- [violation(path: path.empty? ? "atom" : path.join("/"),
17
+ [violation(path: path.empty? ? 'atom' : path.join('/'),
18
18
  message: "atom id #{id.inspect} must start with a letter and " \
19
- "contain only letters, digits, dot, hyphen, or underscore",
19
+ 'contain only letters, digits, dot, hyphen, or underscore',
20
20
  severity: :warning)]
21
21
  end
22
22
  end
@@ -16,7 +16,7 @@ module Chemicalml
16
16
  duplicates = ids.group_by { |x| x }.select { |_, v| v.length > 1 }.keys
17
17
  duplicates.each do |dup|
18
18
  violations << violation(
19
- path: path.empty? ? "molecule[#{node.id}]" : path.join("/"),
19
+ path: path.empty? ? "molecule[#{node.id}]" : path.join('/'),
20
20
  message: "duplicate atom id #{dup.inspect} within molecule #{node.id.inspect}"
21
21
  )
22
22
  end
@@ -33,7 +33,7 @@ module Chemicalml
33
33
  def atom_ids_within(molecule)
34
34
  return [] unless molecule.atom_array
35
35
 
36
- (molecule.atom_array.atoms || []).map { |a| a.id }.compact
36
+ (molecule.atom_array.atoms || []).map(&:id).compact
37
37
  end
38
38
  end
39
39
  end
@@ -5,13 +5,13 @@ module Chemicalml
5
5
  module Molecular
6
6
  module Constraints
7
7
  class AtomMustHaveElementType < Chemicalml::Convention::Constraint::NodeConstraint
8
+ applies_to Chemicalml::Cml::Role::Atom
8
9
  def check_node(node, path)
9
- return [] unless node.is_a?(Chemicalml::Cml::Role::Atom)
10
10
 
11
11
  return [] unless node.element_type.to_s.empty?
12
12
 
13
- [violation(path: path.empty? ? "atom" : path.join("/"),
14
- message: "atom must have an elementType attribute")]
13
+ [violation(path: path.empty? ? 'atom' : path.join('/'),
14
+ message: 'atom must have an elementType attribute')]
15
15
  end
16
16
  end
17
17
  end
@@ -5,13 +5,13 @@ module Chemicalml
5
5
  module Molecular
6
6
  module Constraints
7
7
  class AtomMustHaveId < Chemicalml::Convention::Constraint::NodeConstraint
8
+ applies_to Chemicalml::Cml::Role::Atom
8
9
  def check_node(node, path)
9
- return [] unless node.is_a?(Chemicalml::Cml::Role::Atom)
10
10
 
11
11
  return [] unless node.id.to_s.empty?
12
12
 
13
- [violation(path: path.empty? ? "atom" : path.join("/"),
14
- message: "atom must have an id attribute")]
13
+ [violation(path: path.empty? ? 'atom' : path.join('/'),
14
+ message: 'atom must have an id attribute')]
15
15
  end
16
16
  end
17
17
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chemicalml
4
+ module Convention
5
+ module Molecular
6
+ module Constraints
7
+ # Molecular convention: `<bondArray>` MUST be a child of
8
+ # `<molecule>`. Any other parent is invalid.
9
+ class BondArrayMustBeChildOfMolecule < Chemicalml::Convention::Constraint::NodeConstraint
10
+ def check_node(node, path)
11
+ return [] if molecule?(node)
12
+
13
+ misplaced = node.wire_children.select { |c| bond_array?(c) }
14
+ misplaced.map do |child|
15
+ violation(
16
+ path: (path + [describe(node), describe(child)]).join('/'),
17
+ message: 'bondArray must be a child of molecule, ' \
18
+ "but appears under #{node.element_name.inspect}"
19
+ )
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def molecule?(node)
26
+ node.is_a?(Chemicalml::Cml::Role::Molecule)
27
+ end
28
+
29
+ def bond_array?(node)
30
+ node.is_a?(Chemicalml::Cml::Role::BondArray)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chemicalml
4
+ module Convention
5
+ module Molecular
6
+ module Constraints
7
+ # Molecular convention: a `<bond>` `id` MUST be unique within
8
+ # the eldest containing molecule. Mirrors the existing
9
+ # `AtomIdsUniqueWithinMolecule` constraint for bonds.
10
+ class BondIdsUniqueWithinMolecule < Chemicalml::Convention::Constraint::DocumentConstraint
11
+ def check(document)
12
+ violations = []
13
+ walk_nodes(document) do |node, path|
14
+ next unless molecule?(node)
15
+
16
+ ids = bond_ids_within(node)
17
+ duplicates = ids.group_by { |x| x }.select { |_, v| v.length > 1 }.keys
18
+ duplicates.each do |dup|
19
+ violations << violation(
20
+ path: (path + [describe(node)]).join('/'),
21
+ message: "duplicate bond id #{dup.inspect} within molecule #{node.id.inspect}"
22
+ )
23
+ end
24
+ end
25
+ violations
26
+ end
27
+
28
+ private
29
+
30
+ def molecule?(node)
31
+ node.is_a?(Chemicalml::Cml::Role::Molecule)
32
+ end
33
+
34
+ def bond_ids_within(molecule)
35
+ return [] unless molecule.bond_array
36
+
37
+ (molecule.bond_array.bonds || []).map(&:id).compact
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end