File size: 3,681 Bytes
fdb9863
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
"""Export already audited plots and small public diagnostic receipts.

No inference, fitting, weather download, or modification of source forecasts.
Raw members remain in their immutable source folders; the public receipts give
their hashes and complete-member summaries, not fictional resolved radii.
"""
import argparse
import hashlib
import json
from pathlib import Path
import shutil


def sha(path):
    with path.open('rb') as stream:
        return hashlib.file_digest(stream, 'sha256').hexdigest()


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--plots', type=Path, required=True)
    parser.add_argument('--output', type=Path, required=True)
    args = parser.parse_args()
    args.output.mkdir(parents=True, exist_ok=True)
    verification = json.loads((args.plots/'verification.json').read_text())
    cases = []
    for slug in ('fung_wong', 'soudelor', 'mangkhut', 'meranti'):
        source = args.plots/f'{slug}_structure.json'
        report = json.loads(source.read_text())
        if report['actual_members'] != 50 or not report['physical_50_member_means_verified'] or not report['observed_plus_zero_alignment_verified']:
            raise ValueError('Requires real audited 50-member source')
        # Retain the immutable member-archive digest even though the large raw
        # members are not redundantly copied into the website package.
        public = {k: v for k, v in report.items() if k not in ('member_diagnostics_by_lead', 'source_forecast_folder')}
        public['source_receipt_sha256'] = sha(source)
        public['source_reference'] = 'Original immutable /Volumes/D member archive; original files unchanged.'
        destination = args.output/f'{slug}_structure.json'
        destination.write_text(json.dumps(public, indent=2, allow_nan=False)+'\n')
        image = args.plots/f'{slug}_wind_radius.png'
        matches = [v for k, v in verification['output_sha256'].items() if Path(k).name == image.name]
        if len(matches) != 1 or sha(image) != matches[0]:
            raise ValueError('Changed audited image')
        shutil.copy2(image, args.output/image.name)
        cases.append({'slug': slug, 'name': slug.replace('_', '-').title(), 'storm_id': report['storm_id'],
            'issue_time_utc': report['issue_time_utc'], 'members': 50, 'image': image.name,
            'image_sha256': sha(image), 'receipt': destination.name, 'receipt_sha256': sha(destination),
            'coverage': report['coverage'], 'rmw_method': 'issue-time persistence, not a future RMW forecast'})
    overview = args.plots/'four_storm_wind_candidate.png'
    overview_matches = [v for k, v in verification['output_sha256'].items() if Path(k).name == overview.name]
    if len(overview_matches) != 1 or sha(overview) != overview_matches[0]:
        raise ValueError('Changed audited overview')
    shutil.copy2(overview, args.output/overview.name)
    index = {'schema': 'audited-structure-examples-v1', 'cases': cases,
        'source_verification_sha256': sha(args.plots/'verification.json'),
        'overview': overview.name, 'overview_sha256': sha(overview), 'experimental': True,
        'native_radius_skill_claim': False, 'forecast_weights_changed': False,
        'note': '50 actual member pressure diagnostics. Fung-Wong is a separate fresh replay, not the old MP4 output. '
                'RMW is persistence; USA and JMA wind periods stay separate; no radius-skill score.'}
    (args.output/'index.json').write_text(json.dumps(index, indent=2)+'\n')
    print(json.dumps({'exported_cases': len(cases), 'path': str(args.output), 'actual_members': 50}))


if __name__ == '__main__':
    main()