gradia 1.0.0__py3-none-any.whl → 2.0.0__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.
gradia/__init__.py CHANGED
@@ -1 +1,38 @@
1
- __version__ = "1.0.0"
1
+ """
2
+ Gradia - Local-first ML Training Visualization
3
+
4
+ v2.0.0: Learning Timeline Edition
5
+ """
6
+
7
+ __version__ = "2.0.0"
8
+
9
+ # Core exports
10
+ from .core.scenario import Scenario, ScenarioInferrer
11
+ from .core.config import ConfigManager
12
+ from .core.inspector import Inspector
13
+ from .core.migration import SchemaMigrator, ensure_v2_config
14
+
15
+ # Events module (v2.0)
16
+ from .events import LearningEvent, SampleTracker, TimelineLogger
17
+
18
+ # Trainer
19
+ from .trainer.engine import Trainer
20
+ from .trainer.callbacks import EventLogger
21
+
22
+ __all__ = [
23
+ "__version__",
24
+ # Core
25
+ "Scenario",
26
+ "ScenarioInferrer",
27
+ "ConfigManager",
28
+ "Inspector",
29
+ "SchemaMigrator",
30
+ "ensure_v2_config",
31
+ # Events (v2.0)
32
+ "LearningEvent",
33
+ "SampleTracker",
34
+ "TimelineLogger",
35
+ # Trainer
36
+ "Trainer",
37
+ "EventLogger",
38
+ ]
gradia/cli/main.py CHANGED
@@ -30,7 +30,7 @@ def run(
30
30
  """
31
31
  Starts the gradia training and visualization session.
32
32
  """
33
- console.rule("[bold blue]gradia v1.0.0[/bold blue]")
33
+ console.rule("[bold blue]gradia v2.0.0[/bold blue]")
34
34
 
35
35
  # 1. Inspect
36
36
  path = Path(path).resolve()
gradia/core/config.py CHANGED
@@ -2,55 +2,113 @@ import yaml
2
2
  from pathlib import Path
3
3
  from typing import Any, Dict
4
4
 
5
+ from .migration import SchemaMigrator, SchemaVersion
6
+
7
+
5
8
  class ConfigManager:
6
- """Manages gradia configuration."""
9
+ """Manages gradia configuration with v2.0 migration support."""
7
10
 
11
+ # v2.0 Default Configuration
8
12
  DEFAULT_CONFIG = {
13
+ 'schema_version': SchemaVersion.V2_0.value,
9
14
  'model': {
10
- 'type': 'auto', # auto, linear, random_forest
15
+ 'type': 'auto', # auto, linear, random_forest, sgd, mlp, cnn
11
16
  'params': {}
12
17
  },
13
18
  'training': {
14
19
  'test_split': 0.2,
15
20
  'random_seed': 42,
16
- 'shuffle': True
21
+ 'shuffle': True,
22
+ 'epochs': 10
17
23
  },
18
24
  'scenario': {
19
- 'target': None, # Auto-detect
20
- 'task': None # Auto-detect
21
- }
25
+ 'target': None, # Auto-detect
26
+ 'task': None # Auto-detect
27
+ },
28
+ # v2.0: Learning Timeline configuration
29
+ 'timeline': {
30
+ 'enabled': True,
31
+ 'max_samples': 100,
32
+ 'user_samples': None # List of sample indices to always track
33
+ },
34
+ 'project_name': 'experiment',
35
+ 'save_model': False
22
36
  }
23
37
 
24
38
  def __init__(self, run_dir: str = ".gradia_logs"):
25
39
  self.run_dir = Path(run_dir)
26
40
  self.config_path = self.run_dir / "config.yaml"
41
+ self._migrator = SchemaMigrator()
27
42
 
28
43
  def load_or_create(self, user_overrides: Dict[str, Any] = None) -> Dict[str, Any]:
29
- config = self.DEFAULT_CONFIG.copy()
44
+ config = self._deep_copy(self.DEFAULT_CONFIG)
30
45
 
31
- # Load existing if any (feature for restart, maybe not for MVP run-once)
32
- # For immutable runs, we usually generate NEW config.
33
- # But if gradia.yaml exists in ROOT, we load it.
46
+ # Load existing config if present (for run continuation)
47
+ if self.config_path.exists():
48
+ with open(self.config_path, 'r') as f:
49
+ existing = yaml.safe_load(f) or {}
50
+
51
+ # Migrate to v2 if needed
52
+ result = self._migrator.migrate(existing)
53
+ if result.changes:
54
+ print(f"Config migrated: {', '.join(result.changes)}")
55
+
56
+ self._update_recursive(config, existing)
34
57
 
58
+ # Load root gradia.yaml overrides
35
59
  root_config = Path("gradia.yaml")
36
60
  if root_config.exists():
37
61
  with open(root_config, 'r') as f:
38
- user_config = yaml.safe_load(f)
62
+ user_config = yaml.safe_load(f) or {}
39
63
  self._update_recursive(config, user_config)
40
64
 
65
+ # Apply explicit overrides
41
66
  if user_overrides:
42
67
  self._update_recursive(config, user_overrides)
68
+
69
+ # Ensure v2 fields exist
70
+ config = self._ensure_v2_fields(config)
43
71
 
44
72
  return config
45
73
 
46
74
  def save(self, config: Dict[str, Any]):
47
- self.run_dir.mkdir(exist_ok=True)
75
+ """Save config with schema version marker."""
76
+ self.run_dir.mkdir(parents=True, exist_ok=True)
77
+
78
+ # Ensure schema version is set
79
+ config['schema_version'] = SchemaVersion.V2_0.value
80
+
48
81
  with open(self.config_path, 'w') as f:
49
- yaml.dump(config, f)
82
+ yaml.dump(config, f, default_flow_style=False)
50
83
 
51
84
  def _update_recursive(self, base: Dict, update: Dict):
85
+ """Recursively merge update into base."""
52
86
  for k, v in update.items():
53
87
  if k in base and isinstance(base[k], dict) and isinstance(v, dict):
54
88
  self._update_recursive(base[k], v)
55
89
  else:
56
90
  base[k] = v
91
+
92
+ def _deep_copy(self, d: Dict) -> Dict:
93
+ """Create a deep copy of nested dict."""
94
+ import copy
95
+ return copy.deepcopy(d)
96
+
97
+ def _ensure_v2_fields(self, config: Dict) -> Dict:
98
+ """Ensure all v2.0 required fields exist."""
99
+ # Timeline config
100
+ if 'timeline' not in config:
101
+ config['timeline'] = {
102
+ 'enabled': True,
103
+ 'max_samples': 100,
104
+ 'user_samples': None
105
+ }
106
+
107
+ # Training epochs
108
+ if 'epochs' not in config.get('training', {}):
109
+ config.setdefault('training', {})['epochs'] = 10
110
+
111
+ # Schema version
112
+ config['schema_version'] = SchemaVersion.V2_0.value
113
+
114
+ return config
@@ -0,0 +1,324 @@
1
+ """
2
+ Migration Layer for Gradia v2.0.0
3
+
4
+ Handles backward compatibility with v1.x .gradia_logs runs.
5
+ Supports schema versioning and silent migration.
6
+ """
7
+
8
+ from typing import Dict, Any, Optional, List
9
+ from pathlib import Path
10
+ import json
11
+ import yaml
12
+ from dataclasses import dataclass
13
+ from enum import Enum
14
+
15
+
16
+ class SchemaVersion(str, Enum):
17
+ """Gradia config/log schema versions."""
18
+ V1_0 = "1.0"
19
+ V1_1 = "1.1"
20
+ V1_2 = "1.2"
21
+ V1_3 = "1.3"
22
+ V2_0 = "2.0"
23
+
24
+ @classmethod
25
+ def current(cls) -> "SchemaVersion":
26
+ return cls.V2_0
27
+
28
+
29
+ @dataclass
30
+ class MigrationResult:
31
+ """Result of a migration operation."""
32
+ success: bool
33
+ from_version: str
34
+ to_version: str
35
+ changes: List[str]
36
+ warnings: List[str]
37
+
38
+
39
+ class SchemaMigrator:
40
+ """
41
+ Handles schema migration between Gradia versions.
42
+
43
+ Strategy:
44
+ - Silent upgrades (no user intervention required)
45
+ - Deprecate, don't remove fields
46
+ - Add new fields with sensible defaults
47
+ """
48
+
49
+ def __init__(self):
50
+ self.migrations = {
51
+ ("1.0", "1.1"): self._migrate_1_0_to_1_1,
52
+ ("1.1", "1.2"): self._migrate_1_1_to_1_2,
53
+ ("1.2", "1.3"): self._migrate_1_2_to_1_3,
54
+ ("1.3", "2.0"): self._migrate_1_3_to_2_0,
55
+ }
56
+
57
+ def detect_version(self, config: Dict[str, Any]) -> str:
58
+ """Detect schema version from config structure."""
59
+ # v2.0 has explicit version field
60
+ if "schema_version" in config:
61
+ return config["schema_version"]
62
+
63
+ # v2.0 has timeline config
64
+ if "timeline" in config:
65
+ return "2.0"
66
+
67
+ # v1.3 has project_name and save_model
68
+ if "project_name" in config or "save_model" in config:
69
+ return "1.3"
70
+
71
+ # v1.2 has training.epochs
72
+ if config.get("training", {}).get("epochs"):
73
+ return "1.2"
74
+
75
+ # v1.1 has model.params
76
+ if config.get("model", {}).get("params"):
77
+ return "1.1"
78
+
79
+ # Default to 1.0
80
+ return "1.0"
81
+
82
+ def migrate(self, config: Dict[str, Any], target_version: str = None) -> MigrationResult:
83
+ """
84
+ Migrate config to target version (default: current).
85
+
86
+ Args:
87
+ config: Configuration dictionary
88
+ target_version: Target version string (default: current)
89
+
90
+ Returns:
91
+ MigrationResult with details
92
+ """
93
+ if target_version is None:
94
+ target_version = SchemaVersion.current().value
95
+
96
+ from_version = self.detect_version(config)
97
+ changes = []
98
+ warnings = []
99
+
100
+ if from_version == target_version:
101
+ return MigrationResult(
102
+ success=True,
103
+ from_version=from_version,
104
+ to_version=target_version,
105
+ changes=["No migration needed"],
106
+ warnings=[]
107
+ )
108
+
109
+ # Build migration path
110
+ current = from_version
111
+ version_order = ["1.0", "1.1", "1.2", "1.3", "2.0"]
112
+
113
+ try:
114
+ start_idx = version_order.index(current)
115
+ end_idx = version_order.index(target_version)
116
+ except ValueError as e:
117
+ return MigrationResult(
118
+ success=False,
119
+ from_version=from_version,
120
+ to_version=target_version,
121
+ changes=[],
122
+ warnings=[f"Unknown version: {e}"]
123
+ )
124
+
125
+ if start_idx > end_idx:
126
+ # Downgrade not supported
127
+ return MigrationResult(
128
+ success=False,
129
+ from_version=from_version,
130
+ to_version=target_version,
131
+ changes=[],
132
+ warnings=["Downgrade not supported. Please use a compatible Gradia version."]
133
+ )
134
+
135
+ # Apply migrations sequentially
136
+ for i in range(start_idx, end_idx):
137
+ v_from = version_order[i]
138
+ v_to = version_order[i + 1]
139
+ key = (v_from, v_to)
140
+
141
+ if key in self.migrations:
142
+ migration_fn = self.migrations[key]
143
+ step_changes, step_warnings = migration_fn(config)
144
+ changes.extend(step_changes)
145
+ warnings.extend(step_warnings)
146
+
147
+ # Set version marker
148
+ config["schema_version"] = target_version
149
+
150
+ return MigrationResult(
151
+ success=True,
152
+ from_version=from_version,
153
+ to_version=target_version,
154
+ changes=changes,
155
+ warnings=warnings
156
+ )
157
+
158
+ def _migrate_1_0_to_1_1(self, config: Dict) -> tuple:
159
+ """Add model.params if missing."""
160
+ changes = []
161
+ warnings = []
162
+
163
+ if "model" not in config:
164
+ config["model"] = {"type": "auto", "params": {}}
165
+ changes.append("Added model config with defaults")
166
+ elif "params" not in config["model"]:
167
+ config["model"]["params"] = {}
168
+ changes.append("Added model.params")
169
+
170
+ return changes, warnings
171
+
172
+ def _migrate_1_1_to_1_2(self, config: Dict) -> tuple:
173
+ """Add training.epochs default."""
174
+ changes = []
175
+ warnings = []
176
+
177
+ if "training" not in config:
178
+ config["training"] = {
179
+ "test_split": 0.2,
180
+ "random_seed": 42,
181
+ "shuffle": True,
182
+ "epochs": 10
183
+ }
184
+ changes.append("Added training config with defaults")
185
+ elif "epochs" not in config["training"]:
186
+ config["training"]["epochs"] = 10
187
+ changes.append("Added training.epochs=10 default")
188
+
189
+ return changes, warnings
190
+
191
+ def _migrate_1_2_to_1_3(self, config: Dict) -> tuple:
192
+ """Add project_name and save_model."""
193
+ changes = []
194
+ warnings = []
195
+
196
+ if "project_name" not in config:
197
+ config["project_name"] = "experiment"
198
+ changes.append("Added project_name default")
199
+
200
+ if "save_model" not in config:
201
+ config["save_model"] = False
202
+ changes.append("Added save_model=False default")
203
+
204
+ return changes, warnings
205
+
206
+ def _migrate_1_3_to_2_0(self, config: Dict) -> tuple:
207
+ """Add v2.0 timeline configuration."""
208
+ changes = []
209
+ warnings = []
210
+
211
+ if "timeline" not in config:
212
+ config["timeline"] = {
213
+ "enabled": True,
214
+ "max_samples": 100,
215
+ "user_samples": None
216
+ }
217
+ changes.append("Added timeline config for Learning Timeline feature")
218
+
219
+ # Ensure schema version is set
220
+ config["schema_version"] = "2.0"
221
+ changes.append("Set schema_version to 2.0")
222
+
223
+ return changes, warnings
224
+
225
+
226
+ class RunMigrator:
227
+ """
228
+ Handles migration of existing .gradia_logs run directories.
229
+ """
230
+
231
+ def __init__(self, run_dir: Path):
232
+ self.run_dir = Path(run_dir)
233
+ self.schema_migrator = SchemaMigrator()
234
+
235
+ def needs_migration(self) -> bool:
236
+ """Check if run directory needs migration."""
237
+ config = self._load_config()
238
+ if config is None:
239
+ return False
240
+
241
+ version = self.schema_migrator.detect_version(config)
242
+ return version != SchemaVersion.current().value
243
+
244
+ def migrate(self) -> MigrationResult:
245
+ """Migrate the run directory to current schema."""
246
+ config = self._load_config()
247
+
248
+ if config is None:
249
+ return MigrationResult(
250
+ success=False,
251
+ from_version="unknown",
252
+ to_version=SchemaVersion.current().value,
253
+ changes=[],
254
+ warnings=["No config.yaml found in run directory"]
255
+ )
256
+
257
+ result = self.schema_migrator.migrate(config)
258
+
259
+ if result.success:
260
+ self._save_config(config)
261
+
262
+ # Create migration marker file
263
+ marker_path = self.run_dir / ".migrated"
264
+ marker_path.write_text(f"Migrated from {result.from_version} to {result.to_version}")
265
+
266
+ return result
267
+
268
+ def _load_config(self) -> Optional[Dict]:
269
+ """Load config.yaml from run directory."""
270
+ config_path = self.run_dir / "config.yaml"
271
+
272
+ if not config_path.exists():
273
+ return None
274
+
275
+ with open(config_path, 'r') as f:
276
+ return yaml.safe_load(f)
277
+
278
+ def _save_config(self, config: Dict):
279
+ """Save config.yaml to run directory."""
280
+ config_path = self.run_dir / "config.yaml"
281
+
282
+ with open(config_path, 'w') as f:
283
+ yaml.dump(config, f)
284
+
285
+
286
+ def migrate_all_runs(base_dir: Path = None) -> List[MigrationResult]:
287
+ """
288
+ Migrate all run directories in .gradia_logs.
289
+
290
+ Args:
291
+ base_dir: Base directory containing .gradia_logs (default: cwd)
292
+
293
+ Returns:
294
+ List of migration results
295
+ """
296
+ if base_dir is None:
297
+ base_dir = Path.cwd()
298
+
299
+ logs_dir = base_dir / ".gradia_logs"
300
+ results = []
301
+
302
+ if not logs_dir.exists():
303
+ return results
304
+
305
+ for run_dir in logs_dir.iterdir():
306
+ if run_dir.is_dir() and run_dir.name.startswith("run_"):
307
+ migrator = RunMigrator(run_dir)
308
+ if migrator.needs_migration():
309
+ result = migrator.migrate()
310
+ results.append(result)
311
+ print(f"Migrated {run_dir.name}: {result.from_version} → {result.to_version}")
312
+
313
+ return results
314
+
315
+
316
+ def ensure_v2_config(config: Dict[str, Any]) -> Dict[str, Any]:
317
+ """
318
+ Ensure config has all v2.0 fields with sensible defaults.
319
+
320
+ Use this when loading configs to guarantee v2 compatibility.
321
+ """
322
+ migrator = SchemaMigrator()
323
+ migrator.migrate(config)
324
+ return config
@@ -0,0 +1,17 @@
1
+ """
2
+ Gradia Events Module (v2.0.0)
3
+
4
+ Core abstraction for sample-level learning events that power the Learning Timeline.
5
+ Decouples training logic from visualization and storage.
6
+ """
7
+
8
+ from .models import LearningEvent, EventType
9
+ from .tracker import SampleTracker
10
+ from .logger import TimelineLogger
11
+
12
+ __all__ = [
13
+ "LearningEvent",
14
+ "EventType",
15
+ "SampleTracker",
16
+ "TimelineLogger",
17
+ ]