Files
rpotter6298 9a4ee8e0cf Flatten to a single package directory
The checkout is now astrape/astrape-oracle, not astrape/astrape-oracle/<package>. The package
contents move to the repository root and pyproject maps the package name to
'.' via package-dir, with the subpackages listed explicitly.

The systemd units move out to /srv/astrape/deploy/systemd, outside every
repository, because they describe this deployment rather than any one
component.

Verified both install modes: editable, which is how the deployment runs, and
a plain wheel install, which is what the container build does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 15:05:17 +02:00

159 lines
4.7 KiB
Python

from __future__ import annotations
import argparse
from os import environ
from pathlib import Path
from astrape_ems.classes.env import EnvLoader
from astrape_ems.classes.store import SigenStore
from astrape_oracle.classes.weather.store import WeatherStore
from gibil.classes.usage_sequence_dataset import (
UsageSequenceDatasetBuilder,
UsageSequenceDatasetConfig,
)
from gibil.classes.usage_training import train_model
def main() -> None:
EnvLoader().load()
environ.setdefault("TORCH_CPP_LOG_LEVEL", "ERROR")
args = parse_args()
config = UsageSequenceDatasetConfig.from_env()
builder = UsageSequenceDatasetBuilder(
config=config,
plant_history=SigenStore.from_env(),
weather_history=WeatherStore.from_env(),
)
examples = builder.build(limit=args.limit)
print(f"usage_sequence_examples={len(examples)}")
print(
"minimum_history_hours="
f"{builder.max_past_hours + config.future_hours}"
)
print(f"past_features={len(builder.past_feature_names)}")
for scale in config.past_scales:
print(
f"past_scale={scale.name} "
f"hours={scale.hours} "
f"step_seconds={scale.step_seconds} "
f"steps={builder.past_steps(scale)}"
)
print(f"future_steps={builder.future_steps}")
print(f"future_features={len(builder.future_feature_names)}")
if examples:
first = examples[0]
last = examples[-1]
print(f"first_issued_at={first.issued_at.isoformat()}")
print(f"last_issued_at={last.issued_at.isoformat()}")
for name, rows in first.past_by_scale.items():
print(f"first_past_{name}_shape={len(rows)}x{len(rows[0])}")
token_count = sum(
len(tokens)
for tokens in first.past_tokens_by_scale[name]
)
print(f"first_past_{name}_tokens={token_count}")
print(
"first_future_feature_shape="
f"{len(first.future_features)}x{len(first.future_features[0])}"
)
print(
"first_future_tokens="
f"{sum(len(tokens) for tokens in first.future_tokens)}"
)
print(f"first_targets={len(first.targets)}")
print(
"first_target_preview="
+ ",".join(f"{value:.0f}" for value in first.targets[:8])
)
if args.dry_run:
return
if not examples:
raise SystemExit("No usage sequence examples available for training yet.")
result = train_model(
examples=examples,
builder=builder,
epochs=args.epochs,
batch_size=args.batch_size,
learning_rate=args.learning_rate,
artifact_path=args.artifact_path,
seed=args.seed,
base_artifact_path=args.base_artifact_path,
mode=args.mode,
)
print(f"training_mode={result.mode}")
print(f"training_examples={result.training_examples}")
print(f"validation_examples={result.validation_examples}")
if result.final_train_loss is not None:
print(f"final_train_pinball_loss={result.final_train_loss:.4f}")
if result.final_validation_loss is not None:
print(f"final_validation_pinball_loss={result.final_validation_loss:.4f}")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Build training windows for the sequence usage oracle."
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Only build examples and print dataset shape.",
)
parser.add_argument(
"--limit",
type=int,
default=None,
help="Optional maximum number of examples to build.",
)
parser.add_argument(
"--epochs",
type=int,
default=20,
help="Training epochs.",
)
parser.add_argument(
"--batch-size",
type=int,
default=32,
help="Training batch size.",
)
parser.add_argument(
"--learning-rate",
type=float,
default=0.001,
help="Adam learning rate.",
)
parser.add_argument(
"--artifact-path",
type=Path,
default=Path("models/usage_sequence_tcn_v1.pt"),
help="Where to save the trained TCN artifact.",
)
parser.add_argument(
"--base-artifact-path",
type=Path,
default=None,
help="Existing artifact to continue training from.",
)
parser.add_argument(
"--mode",
choices=("full", "finetune"),
default="full",
help="Training mode recorded in the saved artifact.",
)
parser.add_argument(
"--seed",
type=int,
default=7,
help="Deterministic shuffle seed.",
)
return parser.parse_args()
if __name__ == "__main__":
main()