xtalate 0.1.0.dev0__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 (49) hide show
  1. xtalate/__init__.py +10 -0
  2. xtalate/capabilities/__init__.py +19 -0
  3. xtalate/capabilities/registry.py +126 -0
  4. xtalate/cli/__init__.py +11 -0
  5. xtalate/cli/main.py +431 -0
  6. xtalate/cli/render.py +126 -0
  7. xtalate/conversion/__init__.py +48 -0
  8. xtalate/conversion/engine.py +560 -0
  9. xtalate/conversion/preflight.py +127 -0
  10. xtalate/conversion/report.py +74 -0
  11. xtalate/discovery/__init__.py +28 -0
  12. xtalate/discovery/engine.py +202 -0
  13. xtalate/discovery/report.py +50 -0
  14. xtalate/discovery/sniffer.py +95 -0
  15. xtalate/exporters/__init__.py +34 -0
  16. xtalate/exporters/extxyz.py +140 -0
  17. xtalate/exporters/poscar.py +180 -0
  18. xtalate/exporters/xyz.py +67 -0
  19. xtalate/parsers/__init__.py +31 -0
  20. xtalate/parsers/_common.py +57 -0
  21. xtalate/parsers/extxyz.py +452 -0
  22. xtalate/parsers/poscar.py +428 -0
  23. xtalate/parsers/xyz.py +286 -0
  24. xtalate/py.typed +0 -0
  25. xtalate/recovery/__init__.py +41 -0
  26. xtalate/recovery/engine.py +321 -0
  27. xtalate/recovery/scenarios.py +86 -0
  28. xtalate/registry.py +28 -0
  29. xtalate/schema/__init__.py +40 -0
  30. xtalate/schema/arrays.py +115 -0
  31. xtalate/schema/elements.py +150 -0
  32. xtalate/schema/models.py +283 -0
  33. xtalate/schema/paths.py +88 -0
  34. xtalate/schema/presence.py +152 -0
  35. xtalate/sdk/__init__.py +27 -0
  36. xtalate/sdk/capabilities.py +57 -0
  37. xtalate/sdk/plugins.py +72 -0
  38. xtalate/sdk/results.py +45 -0
  39. xtalate/validation/__init__.py +25 -0
  40. xtalate/validation/engine.py +585 -0
  41. xtalate/validation/report.py +48 -0
  42. xtalate/validation/rethreshold.py +122 -0
  43. xtalate/validation/tolerance.py +125 -0
  44. xtalate-0.1.0.dev0.dist-info/METADATA +161 -0
  45. xtalate-0.1.0.dev0.dist-info/RECORD +49 -0
  46. xtalate-0.1.0.dev0.dist-info/WHEEL +4 -0
  47. xtalate-0.1.0.dev0.dist-info/entry_points.txt +2 -0
  48. xtalate-0.1.0.dev0.dist-info/licenses/LICENSE +201 -0
  49. xtalate-0.1.0.dev0.dist-info/licenses/NOTICE +11 -0
xtalate/cli/render.py ADDED
@@ -0,0 +1,126 @@
1
+ """Human-readable terminal renderers for the report schemas (MASTER_SPEC Appendix A, Part 3 §6.3).
2
+
3
+ Pure functions: report model in, plain-text string out. The CLI is a *thin presenter* (Part 1 §2)
4
+ — these renderers add no scientific logic and invent no information the reports do not already
5
+ carry; `--json` bypasses them entirely and emits the pydantic models verbatim. ASCII glyphs keep
6
+ the output legible in any terminal/pipe (✓/✗ are the one concession, matching the §6.3 inventory).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from xtalate.conversion.report import ConversionReport
12
+ from xtalate.discovery.report import DiscoveryReport
13
+ from xtalate.sdk import CapabilityLevel, FormatCapabilities
14
+ from xtalate.validation.report import ValidationReport
15
+
16
+ _PRESENCE = {"present": "✓", "absent": "✗", "mixed": "◐"}
17
+ _CAP = {
18
+ CapabilityLevel.FULL: "full",
19
+ CapabilityLevel.PARTIAL: "partial",
20
+ CapabilityLevel.NONE: "none",
21
+ }
22
+ _CHECK = {"pass": "✓", "warn": "⚠", "fail": "✗", "skipped": "–"}
23
+
24
+
25
+ def render_discovery(report: DiscoveryReport) -> str:
26
+ fmt = report.format
27
+ lines = [
28
+ f"File: {report.file.get('filename')} ({report.file.get('size_bytes')} bytes)",
29
+ f"Format: {fmt.get('format_name')} [{fmt.get('format_id')}] "
30
+ f"confidence {fmt.get('confidence')}"
31
+ + (" (overridden)" if fmt.get("overridden") else "")
32
+ + (" (ambiguous)" if fmt.get("ambiguous") else ""),
33
+ ]
34
+ struct = report.structure
35
+ lines.append(
36
+ f"Structure: {struct.get('frame_count')} frame(s) × {struct.get('atom_count')} atoms; "
37
+ f"species {', '.join(struct.get('species', []))}"
38
+ )
39
+ lines.append("")
40
+ lines.append("Canonical fields (✓ present / ✗ absent / ◐ mixed · read capability):")
41
+ for field in report.fields:
42
+ glyph = _PRESENCE.get(field.status, "?")
43
+ cap = _CAP.get(field.format_capability, str(field.format_capability))
44
+ detail = f" — {field.detail}" if field.detail else ""
45
+ frames = (
46
+ f" frames {field.present_frames}"
47
+ if field.status == "mixed" and field.present_frames is not None
48
+ else ""
49
+ )
50
+ lines.append(f" {glyph} {field.path:<32} [{cap}]{detail}{frames}")
51
+ if report.extras:
52
+ lines.append("")
53
+ lines.append("Carried-through extras (namespaced, format-specific):")
54
+ lines.extend(f" + {key}" for key in report.extras)
55
+ if report.issues:
56
+ lines.append("")
57
+ lines.append("Parse issues:")
58
+ lines.extend(f" ! [{i.severity}] {i.code}: {i.message}" for i in report.issues)
59
+ return "\n".join(lines)
60
+
61
+
62
+ def render_conversion(report: ConversionReport) -> str:
63
+ lines = [
64
+ f"Conversion Report [{report.stage} · {report.status} · {report.mode}]",
65
+ f" {report.source.get('format_id')} → {report.target.get('format_id')}",
66
+ ]
67
+ if report.status == "refused" and report.refusal is not None:
68
+ lines.append(f" REFUSED [{report.refusal.get('code')}]: {report.refusal.get('message')}")
69
+ for scenario in report.refusal.get("unresolved_scenarios", []):
70
+ lines.append(f" · {scenario.get('scenario')}: {scenario.get('detail')}")
71
+ lines.append(f" preserved ({len(report.preserved)}):")
72
+ for entry in report.preserved:
73
+ lines.append(f" ✓ {entry.path}" + (f" — {entry.detail}" if entry.detail else ""))
74
+ lines.append(f" removed ({len(report.removed)}):")
75
+ for removed in report.removed:
76
+ lines.append(f" ✗ {removed.path} — {removed.reason}")
77
+ if report.supplied:
78
+ lines.append(f" supplied ({len(report.supplied)}):")
79
+ for sup in report.supplied:
80
+ lines.append(f" + {sup.path} (from {sup.from_assumption})")
81
+ if report.assumptions:
82
+ lines.append(f" assumptions ({len(report.assumptions)}):")
83
+ for a in report.assumptions:
84
+ lines.append(f" ~ {a.id} {a.scenario}={a.choice}: {a.description}")
85
+ if report.warnings:
86
+ lines.append(f" warnings ({len(report.warnings)}):")
87
+ for w in report.warnings:
88
+ lines.append(f" ⚠ [{w.source}] {w.message}")
89
+ return "\n".join(lines)
90
+
91
+
92
+ def render_validation(report: ValidationReport) -> str:
93
+ profile = report.tolerance_profile.get("name", "?")
94
+ lines = [f"Validation Report [{report.status}] (tolerance profile: {profile})"]
95
+ for check in report.checks:
96
+ lines.append(f" {_CHECK.get(check.status, '?')} {check.check_id}: {check.message}")
97
+ if report.reparse_issues:
98
+ lines.append(" re-parse issues:")
99
+ lines.extend(f" ! [{i.severity}] {i.code}: {i.message}" for i in report.reparse_issues)
100
+ return "\n".join(lines)
101
+
102
+
103
+ def render_capabilities(declarations: dict[str, dict[str, FormatCapabilities]]) -> str:
104
+ lines: list[str] = []
105
+ for format_id in sorted(declarations):
106
+ directions = declarations[format_id]
107
+ any_caps = next(iter(directions.values()))
108
+ lines.append(f"{any_caps.format_name} [{format_id}]")
109
+ for direction in ("read", "write"):
110
+ caps = directions.get(direction)
111
+ if caps is None:
112
+ lines.append(f" {direction}: (not registered)")
113
+ continue
114
+ frames = "unlimited" if caps.max_frames is None else str(caps.max_frames)
115
+ lines.append(
116
+ f" {direction}: coords={caps.native_coordinate_system}, max_frames={frames}"
117
+ + (f", requires {caps.required_fields}" if caps.required_fields else "")
118
+ )
119
+ for path in sorted(caps.fields):
120
+ cell = caps.fields[path]
121
+ note = f" — {cell.notes}" if cell.notes else ""
122
+ lines.append(f" {path:<40} {_CAP.get(cell.level, str(cell.level))}{note}")
123
+ for note in caps.lossy_notes:
124
+ lines.append(f" (lossy) {note}")
125
+ lines.append("")
126
+ return "\n".join(lines).rstrip()
@@ -0,0 +1,48 @@
1
+ """Conversion Engine — orchestrates parse → capability diff → recovery → export → report.
2
+
3
+ Owns the pre-flight diff (Part 3 §4.3), the ``write_plan`` (Part 4 §1), the
4
+ ``ConversionReport`` (Part 4 §2), and the completeness-invariant runtime assertion
5
+ (review §4.5). Delegates all format logic to the parsers/exporters via their
6
+ ``capabilities()`` declarations. Recovery resolution and the automatic final-step
7
+ validation land in M5; M4 is the happy path plus structured refusal.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from xtalate.conversion.engine import (
13
+ CompletenessInvariantError,
14
+ ConversionEngine,
15
+ ConversionResult,
16
+ build_expected_object,
17
+ )
18
+ from xtalate.conversion.preflight import (
19
+ PreflightDiff,
20
+ build_preflight,
21
+ capability_path,
22
+ )
23
+ from xtalate.conversion.report import (
24
+ Assumption,
25
+ ConversionReport,
26
+ PreservedEntry,
27
+ RemovedEntry,
28
+ ReportWarning,
29
+ SuppliedEntry,
30
+ )
31
+ from xtalate.recovery import UnresolvedScenario
32
+
33
+ __all__ = [
34
+ "Assumption",
35
+ "CompletenessInvariantError",
36
+ "ConversionEngine",
37
+ "ConversionReport",
38
+ "ConversionResult",
39
+ "PreflightDiff",
40
+ "PreservedEntry",
41
+ "RemovedEntry",
42
+ "ReportWarning",
43
+ "SuppliedEntry",
44
+ "UnresolvedScenario",
45
+ "build_expected_object",
46
+ "build_preflight",
47
+ "capability_path",
48
+ ]