Verify portable CPU training path and pin public training assets
Browse files
training/trackformer_1_2/EXECUTION_CHECKS.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"status": "passed",
|
| 3 |
+
"device": "cpu",
|
| 4 |
+
"optimizer_updates": 0,
|
| 5 |
+
"full_training_rerun": false,
|
| 6 |
+
"samples": [
|
| 7 |
+
{
|
| 8 |
+
"native": false,
|
| 9 |
+
"finite_backward": true,
|
| 10 |
+
"attention_gradient_nonzero": true,
|
| 11 |
+
"field_pressure_exact": true,
|
| 12 |
+
"future_input_rejected": true
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"native": true,
|
| 16 |
+
"finite_backward": true,
|
| 17 |
+
"attention_gradient_nonzero": true,
|
| 18 |
+
"field_pressure_exact": true,
|
| 19 |
+
"future_input_rejected": true
|
| 20 |
+
}
|
| 21 |
+
],
|
| 22 |
+
"sampler_all_train_rows": true,
|
| 23 |
+
"counts": {
|
| 24 |
+
"train": 13949,
|
| 25 |
+
"validation": 1041,
|
| 26 |
+
"test": 1195
|
| 27 |
+
},
|
| 28 |
+
"native_counts": {
|
| 29 |
+
"train": 800,
|
| 30 |
+
"validation": 100,
|
| 31 |
+
"test": 100
|
| 32 |
+
},
|
| 33 |
+
"preflight": "passed",
|
| 34 |
+
"new_full_training_run": false
|
| 35 |
+
}
|
training/trackformer_1_2/PUBLICATION.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Immutable publication and execution checks
|
| 2 |
+
|
| 3 |
+
- Source publication on GitHub: https://github.com/yu314-coder/typhoon-predict/commit/ac05e1974ac90ad7819bf0483da8e8637d880525
|
| 4 |
+
- Source and all nine assets on Hugging Face: https://huggingface.co/euler314/typhoon-predict/commit/2d1464e9a310e6a6e8840ef1c23017fdededd57b
|
| 5 |
+
- All asset hashes remain fixed in `assets.json`.
|
| 6 |
+
|
| 7 |
+
Pin the binary download to the completed publication:
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
python training/trackformer_1_2/download_assets.py --revision 2d1464e9a310e6a6e8840ef1c23017fdededd57b
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
The original trainer's CPU preflight passed, including all six input-file hashes, exact split counts, normalization and original warm-start loading (21,452,595 parameters). The portable CPU check passed on one coarse and one native TRAIN example: finite forward/loss/backward, nonzero attention gradient, pressure exactly read from the predicted field, rejection of an added future input, and full TRAIN sampler coverage. Zero optimizer updates were performed. See `EXECUTION_CHECKS.json`. This is an execution check, not a new end-to-end training reproduction or an accuracy claim.
|
| 14 |
+
|
| 15 |
+
```bash
|
| 16 |
+
python training/trackformer_1_2/test_training_cpu.py
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
This test also reconstructs the loader's local basin mmap; use a dedicated writable data copy, never another trainer's data directory. Original GPU-specific tests are archived without rewriting their historical relative-path assumptions in `source/v173/test_model.py`; they were not rerun on GPU for this publication.
|
training/trackformer_1_2/SOURCE_MANIFEST.json
CHANGED
|
@@ -3,40 +3,45 @@
|
|
| 3 |
"unchanged_original_sources": true,
|
| 4 |
"model_weights_modified": false,
|
| 5 |
"files": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
{
|
| 7 |
"path": "training/trackformer_1_2/LICENSE",
|
| 8 |
"sha256": "090a97ebbccef9de77cb3898c473f5c9ada7e140777957f1195fc6ab89b9a313",
|
| 9 |
"bytes": 1083
|
| 10 |
},
|
| 11 |
{
|
| 12 |
-
"path": "training/trackformer_1_2/
|
| 13 |
-
"sha256": "
|
| 14 |
-
"bytes":
|
| 15 |
},
|
| 16 |
{
|
| 17 |
-
"path": "training/trackformer_1_2/
|
| 18 |
-
"sha256": "
|
| 19 |
-
"bytes":
|
| 20 |
},
|
| 21 |
{
|
| 22 |
"path": "training/trackformer_1_2/THIRD_PARTY_NOTICES.md",
|
| 23 |
"sha256": "f764dd203eb124fa6192edd3b7b49a7a546cd5d69d3a8a250589a14ce43e2115",
|
| 24 |
"bytes": 1697
|
| 25 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
{
|
| 27 |
"path": "training/trackformer_1_2/checkpoint-history.json",
|
| 28 |
"sha256": "8b9593a7ed58c1bd28dd27c8049c89a0c5c5644a30bfe5203a4b54c445ff2fb4",
|
| 29 |
"bytes": 459
|
| 30 |
},
|
| 31 |
{
|
| 32 |
-
"path": "training/trackformer_1_2/
|
| 33 |
-
"sha256": "
|
| 34 |
-
"bytes":
|
| 35 |
-
},
|
| 36 |
-
{
|
| 37 |
-
"path": "training/trackformer_1_2/train.py",
|
| 38 |
-
"sha256": "4502a87bfafb11667015070c6fad37b723b4a53723789febda075fba1c7770e8",
|
| 39 |
-
"bytes": 1436
|
| 40 |
},
|
| 41 |
{
|
| 42 |
"path": "training/trackformer_1_2/download_assets.py",
|
|
@@ -44,39 +49,29 @@
|
|
| 44 |
"bytes": 3049
|
| 45 |
},
|
| 46 |
{
|
| 47 |
-
"path": "training/trackformer_1_2/
|
| 48 |
-
"sha256": "
|
| 49 |
-
"bytes":
|
| 50 |
-
},
|
| 51 |
-
{
|
| 52 |
-
"path": "training/trackformer_1_2/source/v173/test_model.py",
|
| 53 |
-
"sha256": "493e0db88a098c89bb4db5e6c577fa24b4b7bcb83ce547dceac35237dbce7206",
|
| 54 |
-
"bytes": 5579
|
| 55 |
-
},
|
| 56 |
-
{
|
| 57 |
-
"path": "training/trackformer_1_2/source/v173/v165_base.py",
|
| 58 |
-
"sha256": "5c9473993a19d0fed00055494b5c1f50c1006aaf46551ff50145f67048c3374a",
|
| 59 |
-
"bytes": 39040
|
| 60 |
},
|
| 61 |
{
|
| 62 |
-
"path": "training/trackformer_1_2/source/
|
| 63 |
-
"sha256": "
|
| 64 |
-
"bytes":
|
| 65 |
},
|
| 66 |
{
|
| 67 |
-
"path": "training/trackformer_1_2/source/
|
| 68 |
-
"sha256": "
|
| 69 |
-
"bytes":
|
| 70 |
},
|
| 71 |
{
|
| 72 |
-
"path": "training/trackformer_1_2/source/
|
| 73 |
-
"sha256": "
|
| 74 |
-
"bytes":
|
| 75 |
},
|
| 76 |
{
|
| 77 |
-
"path": "training/trackformer_1_2/source/
|
| 78 |
-
"sha256": "
|
| 79 |
-
"bytes":
|
| 80 |
},
|
| 81 |
{
|
| 82 |
"path": "training/trackformer_1_2/source/data_preparation/v164_prepare_data.py",
|
|
@@ -84,9 +79,14 @@
|
|
| 84 |
"bytes": 19312
|
| 85 |
},
|
| 86 |
{
|
| 87 |
-
"path": "training/trackformer_1_2/source/data_preparation/
|
| 88 |
-
"sha256": "
|
| 89 |
-
"bytes":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
},
|
| 91 |
{
|
| 92 |
"path": "training/trackformer_1_2/source/data_preparation/v164_structure_research.md",
|
|
@@ -94,29 +94,44 @@
|
|
| 94 |
"bytes": 17119
|
| 95 |
},
|
| 96 |
{
|
| 97 |
-
"path": "training/trackformer_1_2/source/
|
| 98 |
-
"sha256": "
|
| 99 |
-
"bytes":
|
| 100 |
},
|
| 101 |
{
|
| 102 |
-
"path": "training/trackformer_1_2/source/
|
| 103 |
-
"sha256": "
|
| 104 |
-
"bytes":
|
| 105 |
},
|
| 106 |
{
|
| 107 |
-
"path": "training/trackformer_1_2/source/
|
| 108 |
-
"sha256": "
|
| 109 |
-
"bytes":
|
| 110 |
},
|
| 111 |
{
|
| 112 |
-
"path": "training/trackformer_1_2/source/
|
| 113 |
-
"sha256": "
|
| 114 |
-
"bytes":
|
| 115 |
},
|
| 116 |
{
|
| 117 |
-
"path": "training/trackformer_1_2/source/
|
| 118 |
-
"sha256": "
|
| 119 |
-
"bytes":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
}
|
| 121 |
],
|
| 122 |
"assets": [
|
|
|
|
| 3 |
"unchanged_original_sources": true,
|
| 4 |
"model_weights_modified": false,
|
| 5 |
"files": [
|
| 6 |
+
{
|
| 7 |
+
"path": "training/trackformer_1_2/EXECUTION_CHECKS.json",
|
| 8 |
+
"sha256": "baa5800cdca8cbcffdfd5380382d76722613f76169cadf152a4d32553da9c09f",
|
| 9 |
+
"bytes": 738
|
| 10 |
+
},
|
| 11 |
{
|
| 12 |
"path": "training/trackformer_1_2/LICENSE",
|
| 13 |
"sha256": "090a97ebbccef9de77cb3898c473f5c9ada7e140777957f1195fc6ab89b9a313",
|
| 14 |
"bytes": 1083
|
| 15 |
},
|
| 16 |
{
|
| 17 |
+
"path": "training/trackformer_1_2/PUBLICATION.md",
|
| 18 |
+
"sha256": "1bcaa51a1999422925db2a100927985b927b454a2ff4250e318f7049ceec81c8",
|
| 19 |
+
"bytes": 1505
|
| 20 |
},
|
| 21 |
{
|
| 22 |
+
"path": "training/trackformer_1_2/README.md",
|
| 23 |
+
"sha256": "342c43d94710355101ddc5bcad44923f47707f473eb803398f44fe2d77e8ace9",
|
| 24 |
+
"bytes": 6560
|
| 25 |
},
|
| 26 |
{
|
| 27 |
"path": "training/trackformer_1_2/THIRD_PARTY_NOTICES.md",
|
| 28 |
"sha256": "f764dd203eb124fa6192edd3b7b49a7a546cd5d69d3a8a250589a14ce43e2115",
|
| 29 |
"bytes": 1697
|
| 30 |
},
|
| 31 |
+
{
|
| 32 |
+
"path": "training/trackformer_1_2/assets.json",
|
| 33 |
+
"sha256": "a59148e9ba2617599614784b47789c3654df1a7809ee039e78bcd00fe982b211",
|
| 34 |
+
"bytes": 1993
|
| 35 |
+
},
|
| 36 |
{
|
| 37 |
"path": "training/trackformer_1_2/checkpoint-history.json",
|
| 38 |
"sha256": "8b9593a7ed58c1bd28dd27c8049c89a0c5c5644a30bfe5203a4b54c445ff2fb4",
|
| 39 |
"bytes": 459
|
| 40 |
},
|
| 41 |
{
|
| 42 |
+
"path": "training/trackformer_1_2/config.json",
|
| 43 |
+
"sha256": "813f5d05a3dec5ec160b4e91eb80970fb11d88416fc7709ffc9d147fc880316b",
|
| 44 |
+
"bytes": 820
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
},
|
| 46 |
{
|
| 47 |
"path": "training/trackformer_1_2/download_assets.py",
|
|
|
|
| 49 |
"bytes": 3049
|
| 50 |
},
|
| 51 |
{
|
| 52 |
+
"path": "training/trackformer_1_2/requirements.txt",
|
| 53 |
+
"sha256": "37e15a665eff6c8ee94c47ac2da0f7c7e2d699b7e21623b0289da4cf5d3995d0",
|
| 54 |
+
"bytes": 45
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
},
|
| 56 |
{
|
| 57 |
+
"path": "training/trackformer_1_2/source/data_preparation/requirements.txt",
|
| 58 |
+
"sha256": "54918b8f29330299e18b8924e9cd93d05807e7f8cd99b2efd4805e9be94003c1",
|
| 59 |
+
"bytes": 55
|
| 60 |
},
|
| 61 |
{
|
| 62 |
+
"path": "training/trackformer_1_2/source/data_preparation/v164_dataset_contract.md",
|
| 63 |
+
"sha256": "cfd838651e632aba0d2439ba202a59895bd4df22137185974df1d97f0e2525d4",
|
| 64 |
+
"bytes": 4664
|
| 65 |
},
|
| 66 |
{
|
| 67 |
+
"path": "training/trackformer_1_2/source/data_preparation/v164_download_pressure_geography.py",
|
| 68 |
+
"sha256": "b8602cd2fba131a287dc9e25781c8f037d3325fec8a23203576916c2f7d2189c",
|
| 69 |
+
"bytes": 9256
|
| 70 |
},
|
| 71 |
{
|
| 72 |
+
"path": "training/trackformer_1_2/source/data_preparation/v164_global_regional_train.py",
|
| 73 |
+
"sha256": "c0808a1467695d2bc2ad8483aa4f4c6de1463628221024ac559f6204effe7978",
|
| 74 |
+
"bytes": 52581
|
| 75 |
},
|
| 76 |
{
|
| 77 |
"path": "training/trackformer_1_2/source/data_preparation/v164_prepare_data.py",
|
|
|
|
| 79 |
"bytes": 19312
|
| 80 |
},
|
| 81 |
{
|
| 82 |
+
"path": "training/trackformer_1_2/source/data_preparation/v164_prepare_mac.py",
|
| 83 |
+
"sha256": "7e7313f865369d42f67645196b4af0e1b2b84216303f7b2b61e577d3734df79a",
|
| 84 |
+
"bytes": 1309
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"path": "training/trackformer_1_2/source/data_preparation/v164_reuse_impl.py",
|
| 88 |
+
"sha256": "2da0078d4a74faa93f8cf874e42707b01f286406f0ceaa4db1814aab0eceb7d1",
|
| 89 |
+
"bytes": 18414
|
| 90 |
},
|
| 91 |
{
|
| 92 |
"path": "training/trackformer_1_2/source/data_preparation/v164_structure_research.md",
|
|
|
|
| 94 |
"bytes": 17119
|
| 95 |
},
|
| 96 |
{
|
| 97 |
+
"path": "training/trackformer_1_2/source/v173/DESIGN.md",
|
| 98 |
+
"sha256": "163ae39ff572f23c519d65e27ee6a8db23b47df9461670709229ccf95130049a",
|
| 99 |
+
"bytes": 3278
|
| 100 |
},
|
| 101 |
{
|
| 102 |
+
"path": "training/trackformer_1_2/source/v173/baseline_model.py",
|
| 103 |
+
"sha256": "6c05941fd93baaca04508877edc69f8f0394b70e0a64e734ef0d2c1e8832246a",
|
| 104 |
+
"bytes": 10040
|
| 105 |
},
|
| 106 |
{
|
| 107 |
+
"path": "training/trackformer_1_2/source/v173/model.py",
|
| 108 |
+
"sha256": "5dab06c20224cf48c51302122455ee7e16948f977bf4b760fd8aff5d443ab0bc",
|
| 109 |
+
"bytes": 3428
|
| 110 |
},
|
| 111 |
{
|
| 112 |
+
"path": "training/trackformer_1_2/source/v173/test_model.py",
|
| 113 |
+
"sha256": "493e0db88a098c89bb4db5e6c577fa24b4b7bcb83ce547dceac35237dbce7206",
|
| 114 |
+
"bytes": 5579
|
| 115 |
},
|
| 116 |
{
|
| 117 |
+
"path": "training/trackformer_1_2/source/v173/v165_base.py",
|
| 118 |
+
"sha256": "5c9473993a19d0fed00055494b5c1f50c1006aaf46551ff50145f67048c3374a",
|
| 119 |
+
"bytes": 39040
|
| 120 |
+
},
|
| 121 |
+
{
|
| 122 |
+
"path": "training/trackformer_1_2/source/v173/v173_pressure_train.py",
|
| 123 |
+
"sha256": "f40691cec3e787f4ac3c189f647efb0953c1eaa7e03fc86edaf4fee06990b2be",
|
| 124 |
+
"bytes": 11200
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"path": "training/trackformer_1_2/test_training_cpu.py",
|
| 128 |
+
"sha256": "35e9379c40f5928f72f8267dcbdbea892387e0261d74e476730ddcbdf143ba1b",
|
| 129 |
+
"bytes": 3279
|
| 130 |
+
},
|
| 131 |
+
{
|
| 132 |
+
"path": "training/trackformer_1_2/train.py",
|
| 133 |
+
"sha256": "4502a87bfafb11667015070c6fad37b723b4a53723789febda075fba1c7770e8",
|
| 134 |
+
"bytes": 1436
|
| 135 |
}
|
| 136 |
],
|
| 137 |
"assets": [
|
training/trackformer_1_2/test_training_cpu.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CPU-only execution check of real TRAIN inputs; zero optimizer updates."""
|
| 2 |
+
import argparse
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import sys
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def main():
|
| 9 |
+
here = Path(__file__).resolve().parent
|
| 10 |
+
p = argparse.ArgumentParser(description=__doc__)
|
| 11 |
+
p.add_argument('--data-root', type=Path, default=here / 'data')
|
| 12 |
+
p.add_argument('--warm-start', type=Path, default=here / 'checkpoints/v168_epoch_003.pt')
|
| 13 |
+
a = p.parse_args()
|
| 14 |
+
sys.path.insert(0, str(here / 'source/v173'))
|
| 15 |
+
import torch
|
| 16 |
+
from model import CoreForecaster, objective, base
|
| 17 |
+
from v173_pressure_train import warm_start, batches
|
| 18 |
+
torch.set_num_threads(2)
|
| 19 |
+
torch.manual_seed(168)
|
| 20 |
+
base.EvolutionNet.block = lambda self, module, x: module(x)
|
| 21 |
+
m = base.load_reuse(a.data_root / 'manifest.json')
|
| 22 |
+
model = CoreForecaster(m).cpu()
|
| 23 |
+
warm_start(model, a.warm_start, m)
|
| 24 |
+
data = base.ReuseDataset(m, 'train')
|
| 25 |
+
samples = [next(i for i, r in enumerate(data.rows) if (r['patch'] >= 0) == native and r['issue_mask'][1])
|
| 26 |
+
for native in (False, True)]
|
| 27 |
+
results = []
|
| 28 |
+
for index in samples:
|
| 29 |
+
x, y = data[index]
|
| 30 |
+
x = {k: v[None] for k, v in x.items()}
|
| 31 |
+
model.train()
|
| 32 |
+
model.zero_grad(set_to_none=True)
|
| 33 |
+
state = model.initial(x)
|
| 34 |
+
state, prediction = model.step(state)
|
| 35 |
+
target = {k: v[0:1] for k, v in y.items()}
|
| 36 |
+
target.update(previous_pressure=x['issue_intensity'][:, 1],
|
| 37 |
+
previous_pressure_mask=x['issue_mask'][:, 1],
|
| 38 |
+
previous_center=x['center'],
|
| 39 |
+
previous_storm_mask=torch.ones_like(target['storm_mask']))
|
| 40 |
+
loss, terms = objective(prediction, target, x, m)
|
| 41 |
+
assert bool(torch.isfinite(loss))
|
| 42 |
+
loss.backward()
|
| 43 |
+
assert all(bool(torch.isfinite(v.grad).all()) for v in model.parameters() if v.grad is not None)
|
| 44 |
+
assert model.context.output.weight.grad.abs().sum() > 0
|
| 45 |
+
grid = model.core_grid(prediction['center'][:, 0, None, None],
|
| 46 |
+
prediction['center'][:, 1, None, None], state['origin'])
|
| 47 |
+
pressure = base.sample_field(prediction['core'] * model.std[:, :1] + model.mean[:, :1], grid)[:, 0, 0, 0]
|
| 48 |
+
assert torch.equal(pressure, prediction['pressure'])
|
| 49 |
+
try:
|
| 50 |
+
model.initial(dict(x, future_pressure=target['regional']))
|
| 51 |
+
except ValueError:
|
| 52 |
+
pass
|
| 53 |
+
else:
|
| 54 |
+
raise AssertionError('Future target accepted as input')
|
| 55 |
+
results.append(dict(native=bool(x['detail_available'].item()), finite_backward=True,
|
| 56 |
+
attention_gradient_nonzero=True, field_pressure_exact=True,
|
| 57 |
+
future_input_rejected=True))
|
| 58 |
+
indices = base.training_indices(data.rows, 1)
|
| 59 |
+
assert set(indices) == set(range(len(data)))
|
| 60 |
+
for parts, count in batches(indices, data.rows, 10, 4):
|
| 61 |
+
assert sum(map(len, parts)) == count
|
| 62 |
+
assert all(len({data.rows[i]['patch'] >= 0 for i in part}) == 1 for part in parts)
|
| 63 |
+
print(json.dumps(dict(status='passed', device='cpu', optimizer_updates=0,
|
| 64 |
+
full_training_rerun=False, samples=results, sampler_all_train_rows=True,
|
| 65 |
+
counts=m['counts'], native_counts=m['patch_counts']), indent=2))
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
if __name__ == '__main__':
|
| 69 |
+
main()
|