scitex 2.14.0__py3-none-any.whl → 2.15.1__py3-none-any.whl

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 (218) hide show
  1. scitex/__init__.py +47 -0
  2. scitex/_env_loader.py +156 -0
  3. scitex/_mcp_resources/__init__.py +37 -0
  4. scitex/_mcp_resources/_cheatsheet.py +135 -0
  5. scitex/_mcp_resources/_figrecipe.py +138 -0
  6. scitex/_mcp_resources/_formats.py +102 -0
  7. scitex/_mcp_resources/_modules.py +337 -0
  8. scitex/_mcp_resources/_session.py +149 -0
  9. scitex/_mcp_tools/__init__.py +4 -0
  10. scitex/_mcp_tools/audio.py +66 -0
  11. scitex/_mcp_tools/diagram.py +11 -95
  12. scitex/_mcp_tools/introspect.py +191 -0
  13. scitex/_mcp_tools/plt.py +260 -305
  14. scitex/_mcp_tools/scholar.py +74 -0
  15. scitex/_mcp_tools/social.py +244 -0
  16. scitex/_mcp_tools/writer.py +21 -204
  17. scitex/ai/_gen_ai/_PARAMS.py +10 -7
  18. scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
  19. scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
  20. scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
  21. scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
  22. scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
  23. scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
  24. scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
  25. scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
  26. scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
  27. scitex/audio/README.md +40 -36
  28. scitex/audio/__init__.py +127 -59
  29. scitex/audio/_branding.py +185 -0
  30. scitex/audio/_mcp/__init__.py +32 -0
  31. scitex/audio/_mcp/handlers.py +59 -6
  32. scitex/audio/_mcp/speak_handlers.py +238 -0
  33. scitex/audio/_relay.py +225 -0
  34. scitex/audio/engines/elevenlabs_engine.py +6 -1
  35. scitex/audio/mcp_server.py +228 -75
  36. scitex/canvas/README.md +1 -1
  37. scitex/canvas/editor/_dearpygui/__init__.py +25 -0
  38. scitex/canvas/editor/_dearpygui/_editor.py +147 -0
  39. scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
  40. scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
  41. scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
  42. scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
  43. scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
  44. scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
  45. scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
  46. scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
  47. scitex/canvas/editor/_dearpygui/_selection.py +295 -0
  48. scitex/canvas/editor/_dearpygui/_state.py +93 -0
  49. scitex/canvas/editor/_dearpygui/_utils.py +61 -0
  50. scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
  51. scitex/cli/__init__.py +38 -43
  52. scitex/cli/audio.py +76 -27
  53. scitex/cli/capture.py +13 -20
  54. scitex/cli/introspect.py +443 -0
  55. scitex/cli/main.py +198 -109
  56. scitex/cli/mcp.py +60 -34
  57. scitex/cli/scholar/__init__.py +8 -0
  58. scitex/cli/scholar/_crossref_scitex.py +296 -0
  59. scitex/cli/scholar/_fetch.py +25 -3
  60. scitex/cli/social.py +314 -0
  61. scitex/cli/writer.py +117 -0
  62. scitex/config/README.md +1 -1
  63. scitex/config/__init__.py +16 -2
  64. scitex/config/_env_registry.py +191 -0
  65. scitex/diagram/__init__.py +42 -19
  66. scitex/diagram/mcp_server.py +13 -125
  67. scitex/introspect/__init__.py +75 -0
  68. scitex/introspect/_call_graph.py +303 -0
  69. scitex/introspect/_class_hierarchy.py +163 -0
  70. scitex/introspect/_core.py +42 -0
  71. scitex/introspect/_docstring.py +131 -0
  72. scitex/introspect/_examples.py +113 -0
  73. scitex/introspect/_imports.py +271 -0
  74. scitex/introspect/_mcp/__init__.py +37 -0
  75. scitex/introspect/_mcp/handlers.py +208 -0
  76. scitex/introspect/_members.py +151 -0
  77. scitex/introspect/_resolve.py +89 -0
  78. scitex/introspect/_signature.py +131 -0
  79. scitex/introspect/_source.py +80 -0
  80. scitex/introspect/_type_hints.py +172 -0
  81. scitex/io/bundle/README.md +1 -1
  82. scitex/mcp_server.py +98 -5
  83. scitex/plt/__init__.py +248 -550
  84. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
  85. scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  86. scitex/plt/gallery/README.md +1 -1
  87. scitex/plt/utils/_hitmap/__init__.py +82 -0
  88. scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
  89. scitex/plt/utils/_hitmap/_color_application.py +346 -0
  90. scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
  91. scitex/plt/utils/_hitmap/_constants.py +40 -0
  92. scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
  93. scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
  94. scitex/plt/utils/_hitmap/_query.py +113 -0
  95. scitex/plt/utils/_hitmap.py +46 -1616
  96. scitex/plt/utils/_metadata/__init__.py +80 -0
  97. scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
  98. scitex/plt/utils/_metadata/_artists/_base.py +195 -0
  99. scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
  100. scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
  101. scitex/plt/utils/_metadata/_artists/_images.py +80 -0
  102. scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
  103. scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
  104. scitex/plt/utils/_metadata/_artists/_text.py +106 -0
  105. scitex/plt/utils/_metadata/_csv.py +416 -0
  106. scitex/plt/utils/_metadata/_detect.py +225 -0
  107. scitex/plt/utils/_metadata/_legend.py +127 -0
  108. scitex/plt/utils/_metadata/_rounding.py +117 -0
  109. scitex/plt/utils/_metadata/_verification.py +202 -0
  110. scitex/schema/README.md +1 -1
  111. scitex/scholar/__init__.py +8 -0
  112. scitex/scholar/_mcp/crossref_handlers.py +265 -0
  113. scitex/scholar/core/Scholar.py +63 -1700
  114. scitex/scholar/core/_mixins/__init__.py +36 -0
  115. scitex/scholar/core/_mixins/_enrichers.py +270 -0
  116. scitex/scholar/core/_mixins/_library_handlers.py +100 -0
  117. scitex/scholar/core/_mixins/_loaders.py +103 -0
  118. scitex/scholar/core/_mixins/_pdf_download.py +375 -0
  119. scitex/scholar/core/_mixins/_pipeline.py +312 -0
  120. scitex/scholar/core/_mixins/_project_handlers.py +125 -0
  121. scitex/scholar/core/_mixins/_savers.py +69 -0
  122. scitex/scholar/core/_mixins/_search.py +103 -0
  123. scitex/scholar/core/_mixins/_services.py +88 -0
  124. scitex/scholar/core/_mixins/_url_finding.py +105 -0
  125. scitex/scholar/crossref_scitex.py +367 -0
  126. scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  127. scitex/scholar/examples/00_run_all.sh +120 -0
  128. scitex/scholar/jobs/_executors.py +27 -3
  129. scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
  130. scitex/scholar/pdf_download/_cli.py +154 -0
  131. scitex/scholar/pdf_download/strategies/__init__.py +11 -8
  132. scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
  133. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
  134. scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
  135. scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
  136. scitex/scholar/pipelines/_single_steps.py +71 -36
  137. scitex/scholar/storage/_LibraryManager.py +97 -1695
  138. scitex/scholar/storage/_mixins/__init__.py +30 -0
  139. scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
  140. scitex/scholar/storage/_mixins/_library_operations.py +218 -0
  141. scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
  142. scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
  143. scitex/scholar/storage/_mixins/_resolution.py +376 -0
  144. scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
  145. scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
  146. scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
  147. scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
  148. scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
  149. scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
  150. scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
  151. scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
  152. scitex/security/README.md +3 -3
  153. scitex/session/README.md +1 -1
  154. scitex/sh/README.md +1 -1
  155. scitex/social/__init__.py +153 -0
  156. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  157. scitex/template/README.md +1 -1
  158. scitex/template/clone_writer_directory.py +5 -5
  159. scitex/writer/README.md +1 -1
  160. scitex/writer/_mcp/handlers.py +11 -744
  161. scitex/writer/_mcp/tool_schemas.py +5 -335
  162. scitex-2.15.1.dist-info/METADATA +648 -0
  163. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/RECORD +166 -111
  164. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  165. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  166. scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
  167. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
  168. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
  169. scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
  170. scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
  171. scitex/diagram/_compile.py +0 -312
  172. scitex/diagram/_diagram.py +0 -355
  173. scitex/diagram/_mcp/__init__.py +0 -4
  174. scitex/diagram/_mcp/handlers.py +0 -400
  175. scitex/diagram/_mcp/tool_schemas.py +0 -157
  176. scitex/diagram/_presets.py +0 -173
  177. scitex/diagram/_schema.py +0 -182
  178. scitex/diagram/_split.py +0 -278
  179. scitex/plt/_mcp/__init__.py +0 -4
  180. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  181. scitex/plt/_mcp/_handlers_figure.py +0 -195
  182. scitex/plt/_mcp/_handlers_plot.py +0 -252
  183. scitex/plt/_mcp/_handlers_style.py +0 -219
  184. scitex/plt/_mcp/handlers.py +0 -74
  185. scitex/plt/_mcp/tool_schemas.py +0 -497
  186. scitex/plt/mcp_server.py +0 -231
  187. scitex/scholar/data/.gitkeep +0 -0
  188. scitex/scholar/data/README.md +0 -44
  189. scitex/scholar/data/bib_files/bibliography.bib +0 -1952
  190. scitex/scholar/data/bib_files/neurovista.bib +0 -277
  191. scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
  192. scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
  193. scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
  194. scitex/scholar/data/bib_files/openaccess.bib +0 -89
  195. scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
  196. scitex/scholar/data/bib_files/pac.bib +0 -698
  197. scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
  198. scitex/scholar/data/bib_files/pac_processed.bib +0 -0
  199. scitex/scholar/data/bib_files/pac_titles.txt +0 -75
  200. scitex/scholar/data/bib_files/paywalled.bib +0 -98
  201. scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
  202. scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
  203. scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
  204. scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
  205. scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
  206. scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
  207. scitex/scholar/data/bib_files/test_seizure.bib +0 -46
  208. scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
  209. scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
  210. scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
  211. scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
  212. scitex/scholar/data/impact_factor.db +0 -0
  213. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  214. scitex/scholar/examples/dev.py +0 -38
  215. scitex-2.14.0.dist-info/METADATA +0 -1238
  216. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/WHEEL +0 -0
  217. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/entry_points.txt +0 -0
  218. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,173 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- # Timestamp: 2025-12-15
4
- # Author: ywatanabe / Claude
5
- # File: scitex/diagram/_presets.py
6
-
7
- """
8
- Presets for common diagram types in scientific papers.
9
-
10
- Each preset defines rules for compiling the semantic spec to backend formats.
11
- These encode domain knowledge about "what makes a good paper figure."
12
- """
13
-
14
- from dataclasses import dataclass
15
- from typing import Dict, Any, List
16
-
17
-
18
- @dataclass
19
- class DiagramPreset:
20
- """Rules for compiling a diagram type."""
21
-
22
- # Mermaid settings
23
- mermaid_direction: str # TB, LR, RL, BT
24
- mermaid_theme: Dict[str, str]
25
-
26
- # Graphviz settings
27
- graphviz_rankdir: str # TB, LR, RL, BT
28
- graphviz_ranksep: float
29
- graphviz_nodesep: float
30
-
31
- # Spacing mappings
32
- spacing_map: Dict[str, Dict[str, float]]
33
-
34
- # Shape mappings (semantic -> backend)
35
- mermaid_shapes: Dict[str, str]
36
- graphviz_shapes: Dict[str, str]
37
-
38
- # Emphasis styles (colors, borders)
39
- emphasis_styles: Dict[str, Dict[str, str]]
40
-
41
-
42
- # Workflow preset: sequential processes, emphasize flow
43
- WORKFLOW_PRESET = DiagramPreset(
44
- mermaid_direction="LR", # Left-to-right for workflows
45
- mermaid_theme={
46
- "primaryColor": "#1a2634",
47
- "primaryTextColor": "#e0e0e0",
48
- "primaryBorderColor": "#3a4a5a",
49
- "lineColor": "#5a9fcf",
50
- },
51
- graphviz_rankdir="LR",
52
- graphviz_ranksep=0.8,
53
- graphviz_nodesep=0.5,
54
- spacing_map={
55
- "tight": {"ranksep": 0.3, "nodesep": 0.2}, # Publication: minimal
56
- "compact": {"ranksep": 0.4, "nodesep": 0.3},
57
- "medium": {"ranksep": 0.8, "nodesep": 0.5},
58
- "large": {"ranksep": 1.2, "nodesep": 0.8},
59
- },
60
- mermaid_shapes={
61
- "box": '["__LABEL__"]',
62
- "rounded": '("__LABEL__")',
63
- "diamond": '{"__LABEL__"}',
64
- "circle": '(("__LABEL__"))',
65
- "stadium": '(["__LABEL__"])',
66
- },
67
- graphviz_shapes={
68
- "box": "box",
69
- "rounded": "box", # with style=rounded
70
- "diamond": "diamond",
71
- "circle": "circle",
72
- "stadium": "box", # with style=rounded
73
- },
74
- emphasis_styles={
75
- "primary": {"fill": "#0d4a6b", "stroke": "#5a9fcf", "stroke-width": "2px"},
76
- "success": {"fill": "#ccffcc", "stroke": "#00cc00"},
77
- "warning": {"fill": "#ffcccc", "stroke": "#cc0000"},
78
- "muted": {"fill": "#f0f0f0", "stroke": "#999999"},
79
- "normal": {"fill": "#1a2634", "stroke": "#3a4a5a"},
80
- },
81
- )
82
-
83
- # Decision tree preset: top-to-bottom with branches
84
- DECISION_PRESET = DiagramPreset(
85
- mermaid_direction="TB",
86
- mermaid_theme={
87
- "primaryColor": "#f5f5f5",
88
- "primaryTextColor": "#333333",
89
- "primaryBorderColor": "#666666",
90
- "lineColor": "#666666",
91
- },
92
- graphviz_rankdir="TB",
93
- graphviz_ranksep=1.0,
94
- graphviz_nodesep=0.6,
95
- spacing_map={
96
- "compact": {"ranksep": 0.6, "nodesep": 0.4},
97
- "medium": {"ranksep": 1.0, "nodesep": 0.6},
98
- "large": {"ranksep": 1.5, "nodesep": 1.0},
99
- },
100
- mermaid_shapes={
101
- "box": '["__LABEL__"]',
102
- "rounded": '("__LABEL__")',
103
- "diamond": '{"__LABEL__"}',
104
- "circle": '(("__LABEL__"))',
105
- "stadium": '(["__LABEL__"])',
106
- },
107
- graphviz_shapes={
108
- "box": "box",
109
- "rounded": "box",
110
- "diamond": "diamond",
111
- "circle": "circle",
112
- "stadium": "box",
113
- },
114
- emphasis_styles={
115
- "primary": {"fill": "#e6f3ff", "stroke": "#0066cc", "stroke-width": "2px"},
116
- "success": {"fill": "#e6ffe6", "stroke": "#00aa00"},
117
- "warning": {"fill": "#ffe6e6", "stroke": "#cc0000"},
118
- "muted": {"fill": "#f0f0f0", "stroke": "#aaaaaa"},
119
- "normal": {"fill": "#ffffff", "stroke": "#666666"},
120
- },
121
- )
122
-
123
- # Pipeline preset: strict horizontal stages
124
- PIPELINE_PRESET = DiagramPreset(
125
- mermaid_direction="LR",
126
- mermaid_theme={
127
- "primaryColor": "#ffffff",
128
- "primaryTextColor": "#333333",
129
- "primaryBorderColor": "#0066cc",
130
- "lineColor": "#0066cc",
131
- },
132
- graphviz_rankdir="LR",
133
- graphviz_ranksep=1.2,
134
- graphviz_nodesep=0.4,
135
- spacing_map={
136
- "compact": {"ranksep": 0.8, "nodesep": 0.3},
137
- "medium": {"ranksep": 1.2, "nodesep": 0.4},
138
- "large": {"ranksep": 1.8, "nodesep": 0.6},
139
- },
140
- mermaid_shapes={
141
- "box": '["__LABEL__"]',
142
- "rounded": '("__LABEL__")',
143
- "diamond": '{"__LABEL__"}',
144
- "circle": '(("__LABEL__"))',
145
- "stadium": '(["__LABEL__"])',
146
- },
147
- graphviz_shapes={
148
- "box": "box",
149
- "rounded": "box",
150
- "diamond": "diamond",
151
- "circle": "circle",
152
- "stadium": "box",
153
- },
154
- emphasis_styles={
155
- "primary": {"fill": "#e6f0ff", "stroke": "#0044aa", "stroke-width": "2px"},
156
- "success": {"fill": "#e6ffe6", "stroke": "#00aa00"},
157
- "warning": {"fill": "#fff3e6", "stroke": "#ff8800"},
158
- "muted": {"fill": "#f5f5f5", "stroke": "#cccccc"},
159
- "normal": {"fill": "#ffffff", "stroke": "#0066cc"},
160
- },
161
- )
162
-
163
-
164
- def get_preset(diagram_type: str) -> DiagramPreset:
165
- """Get preset for diagram type."""
166
- presets = {
167
- "workflow": WORKFLOW_PRESET,
168
- "decision": DECISION_PRESET,
169
- "pipeline": PIPELINE_PRESET,
170
- "hierarchy": DECISION_PRESET, # Same as decision for now
171
- "comparison": WORKFLOW_PRESET, # Override direction in compiler
172
- }
173
- return presets.get(diagram_type.lower(), WORKFLOW_PRESET)
scitex/diagram/_schema.py DELETED
@@ -1,182 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- # Timestamp: 2025-12-15
4
- # Author: ywatanabe / Claude
5
- # File: scitex/diagram/_schema.py
6
-
7
- """
8
- Schema definitions for SciTeX Diagram.
9
-
10
- The schema defines paper-specific constraints that Mermaid/Graphviz don't know:
11
- - Paper layout (single/double column, max width)
12
- - Reading direction preferences
13
- - Node emphasis for scientific communication
14
- - Semantic layer grouping
15
- """
16
-
17
- from dataclasses import dataclass, field
18
- from enum import Enum
19
- from typing import List, Dict, Optional, Literal
20
-
21
-
22
- class DiagramType(Enum):
23
- """Semantic type of diagram - affects layout strategy."""
24
- WORKFLOW = "workflow" # Sequential process, prefer LR/TB flow
25
- DECISION = "decision" # Decision tree, prefer TB with branches
26
- PIPELINE = "pipeline" # Data pipeline, strict LR with stages
27
- HIERARCHY = "hierarchy" # Tree structure, TB with levels
28
- COMPARISON = "comparison" # Side-by-side, two columns
29
-
30
-
31
- class ColumnLayout(Enum):
32
- """Paper column layout."""
33
- SINGLE = "single" # Full width (~170mm)
34
- DOUBLE = "double" # Half width (~85mm)
35
-
36
-
37
- class SpacingLevel(Enum):
38
- """Abstract spacing levels - mapped to backend-specific values."""
39
- TIGHT = "tight" # Publication: minimal whitespace
40
- COMPACT = "compact"
41
- MEDIUM = "medium"
42
- LARGE = "large"
43
-
44
-
45
- class PaperMode(Enum):
46
- """Paper mode affects layout density and edge visibility."""
47
- DRAFT = "draft" # Full arrows, visible bidirectional, medium spacing
48
- PUBLICATION = "publication" # Compact, return edges hidden/dotted
49
-
50
-
51
- @dataclass
52
- class PaperConstraints:
53
- """Paper-specific constraints that affect layout."""
54
- column: ColumnLayout = ColumnLayout.SINGLE
55
- max_width_mm: int = 170
56
- reading_direction: Literal["left_to_right", "top_to_bottom"] = "left_to_right"
57
- mode: PaperMode = PaperMode.DRAFT # draft: full details, publication: compact
58
- emphasize: List[str] = field(default_factory=list) # Node IDs to highlight
59
-
60
- # Scientific communication hints
61
- main_flow: List[str] = field(default_factory=list) # Critical path nodes
62
- secondary_flow: List[str] = field(default_factory=list) # Supporting elements
63
- return_edges: List[tuple] = field(default_factory=list) # Edges to hide in publication
64
-
65
-
66
- @dataclass
67
- class LayoutHints:
68
- """Abstract layout hints - compiled to backend directives."""
69
- layers: List[List[str]] = field(default_factory=list) # Nodes grouped by rank
70
- alignment: Dict[str, str] = field(default_factory=dict) # Node alignment hints
71
- layer_gap: SpacingLevel = SpacingLevel.MEDIUM
72
- node_gap: SpacingLevel = SpacingLevel.MEDIUM
73
-
74
- # Subgraph organization
75
- groups: Dict[str, List[str]] = field(default_factory=dict) # Named groups
76
-
77
-
78
- @dataclass
79
- class NodeSpec:
80
- """Specification for a single node."""
81
- id: str
82
- label: str
83
- shape: Literal["box", "rounded", "diamond", "circle", "stadium"] = "box"
84
- emphasis: Literal["normal", "primary", "success", "warning", "muted"] = "normal"
85
-
86
- def short_label(self, max_chars: int = 20) -> str:
87
- """Return truncated label for compact layouts."""
88
- if len(self.label) <= max_chars:
89
- return self.label
90
- return self.label[:max_chars-3] + "..."
91
-
92
-
93
- @dataclass
94
- class EdgeSpec:
95
- """Specification for an edge between nodes."""
96
- source: str
97
- target: str
98
- label: Optional[str] = None
99
- style: Literal["solid", "dashed", "dotted"] = "solid"
100
- arrow: Literal["normal", "none", "open"] = "normal"
101
-
102
-
103
- @dataclass
104
- class DiagramSpec:
105
- """Complete diagram specification - the semantic layer."""
106
-
107
- # Metadata
108
- type: DiagramType = DiagramType.WORKFLOW
109
- title: str = ""
110
-
111
- # Paper constraints
112
- paper: PaperConstraints = field(default_factory=PaperConstraints)
113
-
114
- # Layout hints
115
- layout: LayoutHints = field(default_factory=LayoutHints)
116
-
117
- # Content
118
- nodes: List[NodeSpec] = field(default_factory=list)
119
- edges: List[EdgeSpec] = field(default_factory=list)
120
-
121
- # Theme
122
- theme: Dict[str, str] = field(default_factory=dict)
123
-
124
- @classmethod
125
- def from_dict(cls, data: dict) -> "DiagramSpec":
126
- """Create DiagramSpec from dictionary (parsed YAML)."""
127
- spec = cls()
128
-
129
- # Parse type
130
- if "type" in data:
131
- spec.type = DiagramType(data["type"])
132
-
133
- spec.title = data.get("title", "")
134
-
135
- # Parse paper constraints
136
- if "paper" in data:
137
- p = data["paper"]
138
- spec.paper = PaperConstraints(
139
- column=ColumnLayout(p.get("column", "single")),
140
- max_width_mm=p.get("max_width_mm", 170),
141
- reading_direction=p.get("reading_direction", "left_to_right"),
142
- mode=PaperMode(p.get("mode", "draft")),
143
- emphasize=p.get("emphasize", []),
144
- main_flow=p.get("main_flow", []),
145
- secondary_flow=p.get("secondary_flow", []),
146
- return_edges=[tuple(e) for e in p.get("return_edges", [])],
147
- )
148
-
149
- # Parse layout hints
150
- if "layout" in data:
151
- lt = data["layout"]
152
- spec.layout = LayoutHints(
153
- layers=lt.get("layers", []),
154
- alignment=lt.get("alignment", {}),
155
- layer_gap=SpacingLevel(lt.get("layer_gap", "medium")),
156
- node_gap=SpacingLevel(lt.get("node_gap", "medium")),
157
- groups=lt.get("groups", {}),
158
- )
159
-
160
- # Parse nodes
161
- for n in data.get("nodes", []):
162
- spec.nodes.append(NodeSpec(
163
- id=n["id"],
164
- label=n.get("label", n["id"]),
165
- shape=n.get("shape", "box"),
166
- emphasis=n.get("emphasis", "normal"),
167
- ))
168
-
169
- # Parse edges
170
- for e in data.get("edges", []):
171
- spec.edges.append(EdgeSpec(
172
- source=e["from"] if "from" in e else e["source"],
173
- target=e["to"] if "to" in e else e["target"],
174
- label=e.get("label"),
175
- style=e.get("style", "solid"),
176
- arrow=e.get("arrow", "normal"),
177
- ))
178
-
179
- # Theme
180
- spec.theme = data.get("theme", {})
181
-
182
- return spec
scitex/diagram/_split.py DELETED
@@ -1,278 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- # Timestamp: 2025-12-15
4
- # Author: ywatanabe / Claude
5
- # File: scitex/diagram/_split.py
6
-
7
- """
8
- Auto-split large diagrams into multiple figures.
9
-
10
- Strategies:
11
- - by_groups: Split by existing layout.groups (deterministic, paper-friendly)
12
- - by_articulation: Split at hub nodes (graph-theoretic)
13
-
14
- The split preserves "ghost nodes" at boundaries for visual continuity.
15
- """
16
-
17
- from dataclasses import dataclass, field
18
- from typing import List, Dict, Set, Optional, Tuple
19
- from copy import deepcopy
20
- from enum import Enum
21
-
22
- from scitex.diagram._schema import DiagramSpec, NodeSpec, EdgeSpec
23
-
24
-
25
- class SplitStrategy(Enum):
26
- BY_GROUPS = "by_groups" # Split by layout.groups
27
- BY_ARTICULATION = "by_articulation" # Split at hub nodes
28
-
29
-
30
- @dataclass
31
- class SplitConfig:
32
- """Configuration for auto-splitting."""
33
- enabled: bool = False
34
- max_nodes: int = 12 # Split if more nodes than this
35
- strategy: SplitStrategy = SplitStrategy.BY_GROUPS
36
- keep_hubs: bool = True # Show hub nodes in both parts
37
- ghost_style: str = "muted" # Style for ghost nodes
38
-
39
-
40
- @dataclass
41
- class SplitResult:
42
- """Result of splitting a diagram."""
43
- figures: List[DiagramSpec]
44
- labels: List[str] # Figure labels (A, B, C, ...)
45
- cut_nodes: Set[str] # Nodes that appear in multiple figures
46
-
47
-
48
- def split_diagram(
49
- spec: DiagramSpec,
50
- config: Optional[SplitConfig] = None,
51
- group_assignments: Optional[List[List[str]]] = None,
52
- ) -> SplitResult:
53
- """
54
- Split a diagram into multiple figures.
55
-
56
- Parameters
57
- ----------
58
- spec : DiagramSpec
59
- Original diagram specification.
60
- config : SplitConfig, optional
61
- Split configuration.
62
- group_assignments : List[List[str]], optional
63
- Manual group assignments for splitting.
64
- If provided, overrides automatic detection.
65
-
66
- Returns
67
- -------
68
- SplitResult
69
- List of split diagram specifications.
70
- """
71
- if config is None:
72
- config = SplitConfig(enabled=True)
73
-
74
- # Check if split is needed
75
- if not config.enabled or len(spec.nodes) <= config.max_nodes:
76
- return SplitResult(figures=[spec], labels=[""], cut_nodes=set())
77
-
78
- # Determine groups to split by
79
- if group_assignments:
80
- groups = group_assignments
81
- elif config.strategy == SplitStrategy.BY_GROUPS:
82
- groups = _split_by_groups(spec, max_nodes=config.max_nodes)
83
- else: # BY_ARTICULATION
84
- groups = _split_by_articulation(spec)
85
-
86
- # Create split figures
87
- figures = []
88
- labels = []
89
- cut_nodes = set()
90
-
91
- for i, group_nodes in enumerate(groups):
92
- fig, cuts = _create_split_figure(spec, group_nodes, config)
93
- figures.append(fig)
94
- labels.append(chr(ord('A') + i))
95
- cut_nodes.update(cuts)
96
-
97
- return SplitResult(figures=figures, labels=labels, cut_nodes=cut_nodes)
98
-
99
-
100
- def _split_by_groups(spec: DiagramSpec, max_nodes: int = 12) -> List[List[str]]:
101
- """
102
- Split by existing layout.groups using greedy packing.
103
-
104
- Packs groups into figures until max_nodes is exceeded,
105
- then starts a new figure.
106
-
107
- Returns list of node ID lists, one per split figure.
108
- """
109
- if not spec.layout.groups:
110
- # No groups defined - try to split in half
111
- node_ids = [n.id for n in spec.nodes]
112
- mid = len(node_ids) // 2
113
- return [node_ids[:mid], node_ids[mid:]]
114
-
115
- # Group keys in order
116
- group_names = list(spec.layout.groups.keys())
117
-
118
- # Greedy packing: add groups until max_nodes exceeded
119
- figures = []
120
- current_figure = []
121
- current_count = 0
122
-
123
- for group_name in group_names:
124
- group_nodes = spec.layout.groups[group_name]
125
- group_size = len(group_nodes)
126
-
127
- # If adding this group exceeds max and we have something, start new figure
128
- if current_count + group_size > max_nodes and current_figure:
129
- figures.append(current_figure)
130
- current_figure = []
131
- current_count = 0
132
-
133
- # Add group to current figure
134
- current_figure.extend(group_nodes)
135
- current_count += group_size
136
-
137
- # Don't forget the last figure
138
- if current_figure:
139
- figures.append(current_figure)
140
-
141
- # Add ungrouped nodes to first figure
142
- grouped = set()
143
- for fig in figures:
144
- grouped.update(fig)
145
- for n in spec.nodes:
146
- if n.id not in grouped:
147
- if figures:
148
- figures[0].append(n.id)
149
- else:
150
- figures.append([n.id])
151
-
152
- # Ensure at least 2 figures if we have enough nodes
153
- if len(figures) == 1 and len(figures[0]) > max_nodes:
154
- # Force split in half
155
- nodes = figures[0]
156
- mid = len(nodes) // 2
157
- figures = [nodes[:mid], nodes[mid:]]
158
-
159
- return figures
160
-
161
-
162
- def _split_by_articulation(spec: DiagramSpec) -> List[List[str]]:
163
- """
164
- Split at articulation points (hub nodes).
165
-
166
- This finds nodes that, if removed, would disconnect the graph.
167
- These are natural split points for large diagrams.
168
- """
169
- # Build adjacency
170
- adj: Dict[str, Set[str]] = {n.id: set() for n in spec.nodes}
171
- for e in spec.edges:
172
- adj[e.source].add(e.target)
173
- adj[e.target].add(e.source)
174
-
175
- # Find node with most connections (hub)
176
- hub = max(adj.keys(), key=lambda x: len(adj[x]))
177
-
178
- # BFS from first node, stopping at hub
179
- visited = {hub} # Block the hub
180
- node_ids = [n.id for n in spec.nodes if n.id != hub]
181
-
182
- if not node_ids:
183
- return [[hub]]
184
-
185
- # Find components when hub is removed
186
- components = []
187
- for start in node_ids:
188
- if start in visited:
189
- continue
190
- component = []
191
- queue = [start]
192
- while queue:
193
- curr = queue.pop(0)
194
- if curr in visited:
195
- continue
196
- visited.add(curr)
197
- component.append(curr)
198
- for neighbor in adj[curr]:
199
- if neighbor not in visited:
200
- queue.append(neighbor)
201
- if component:
202
- components.append(component)
203
-
204
- # Add hub to each component (as ghost)
205
- for comp in components:
206
- comp.append(hub)
207
-
208
- return components if components else [[n.id for n in spec.nodes]]
209
-
210
-
211
- def _create_split_figure(
212
- spec: DiagramSpec,
213
- node_ids: List[str],
214
- config: SplitConfig,
215
- ) -> Tuple[DiagramSpec, Set[str]]:
216
- """
217
- Create a split figure containing specified nodes.
218
-
219
- Returns (figure_spec, ghost_node_ids).
220
- """
221
- node_id_set = set(node_ids)
222
-
223
- # Find edges that cross the boundary
224
- boundary_nodes = set()
225
- for edge in spec.edges:
226
- src_in = edge.source in node_id_set
227
- tgt_in = edge.target in node_id_set
228
- if src_in and not tgt_in:
229
- if config.keep_hubs:
230
- boundary_nodes.add(edge.target)
231
- elif tgt_in and not src_in:
232
- if config.keep_hubs:
233
- boundary_nodes.add(edge.source)
234
-
235
- # Create new spec
236
- new_spec = DiagramSpec(
237
- type=spec.type,
238
- title=spec.title,
239
- paper=deepcopy(spec.paper),
240
- layout=deepcopy(spec.layout),
241
- theme=dict(spec.theme),
242
- )
243
-
244
- # Filter nodes
245
- node_map = {n.id: n for n in spec.nodes}
246
- for node_id in node_ids:
247
- if node_id in node_map:
248
- new_spec.nodes.append(deepcopy(node_map[node_id]))
249
-
250
- # Add ghost nodes (boundary nodes not in this split)
251
- for ghost_id in boundary_nodes:
252
- if ghost_id in node_map and ghost_id not in node_id_set:
253
- ghost = deepcopy(node_map[ghost_id])
254
- ghost.emphasis = config.ghost_style
255
- ghost.label = f"→ {ghost.label}" # Mark as continuation
256
- new_spec.nodes.append(ghost)
257
-
258
- # Filter edges
259
- all_ids = node_id_set | boundary_nodes
260
- for edge in spec.edges:
261
- if edge.source in all_ids and edge.target in all_ids:
262
- new_spec.edges.append(deepcopy(edge))
263
-
264
- # Filter groups
265
- new_spec.layout.groups = {}
266
- for group_name, group_nodes in spec.layout.groups.items():
267
- filtered = [n for n in group_nodes if n in all_ids]
268
- if filtered:
269
- new_spec.layout.groups[group_name] = filtered
270
-
271
- # Filter layers
272
- new_spec.layout.layers = []
273
- for layer in spec.layout.layers:
274
- filtered = [n for n in layer if n in all_ids]
275
- if filtered:
276
- new_spec.layout.layers.append(filtered)
277
-
278
- return new_spec, boundary_nodes
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env python3
2
- # File: __init__.py
3
- """MCP server components."""
4
-