toshi-hazard-post 0.7.1__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.
scripts/__init__.py ADDED
File without changes
scripts/calc_time.py ADDED
@@ -0,0 +1,34 @@
1
+ from pathlib import Path
2
+ import sys
3
+
4
+ def main(log_filepath):
5
+ log_filepath = Path(log_filepath)
6
+
7
+ times_jobtable = []
8
+ times_batchtable = []
9
+ times_calc = []
10
+ with log_filepath.open() as f:
11
+ for line in f.readlines():
12
+ if "starting" in line and "calculations with" in line:
13
+ nproc = int(line.split(' ')[-2])
14
+ elif "time to create batch table" in line:
15
+ times_batchtable.append(float(line.split(' ')[-2]))
16
+ elif "time to create job table" in line:
17
+ times_jobtable.append(float(line.split(' ')[-2]))
18
+ elif "time to perform one aggregation" in line:
19
+ times_calc.append(float(line.split(' ')[-2]))
20
+ elif "time to perform aggregations after job setup" in line:
21
+ total_time = float(line.split(' ')[-1])
22
+
23
+ print(f"Number of workers: {nproc}")
24
+ print(f"Total number of partitions used: {len(times_batchtable)}")
25
+ print(f"Total number of aggregate calculations: {len(times_calc)}")
26
+ print(f"Mean Partition Table Creation Time: {sum(times_batchtable)/len(times_batchtable):.02f} seconds")
27
+ print(f"Mean Job Table Creation Time: {sum(times_jobtable)/len(times_jobtable):.02f} seconds")
28
+ print(f"Mean Agg Calc Time: {sum(times_calc)/len(times_calc):.02f} seconds")
29
+ print(f"Total Time: {total_time} seconds")
30
+ print(f"Mean time per calculation (including I/O): {nproc * total_time/len(times_calc):.02f} CPU seconds")
31
+
32
+ if __name__ == "__main__":
33
+ filepath = sys.argv[1]
34
+ main(filepath)
scripts/cli.py ADDED
@@ -0,0 +1,36 @@
1
+ import logging
2
+ import os
3
+
4
+ import click
5
+
6
+ from toshi_hazard_post.aggregation import run_aggregation
7
+ from toshi_hazard_post.aggregation_args import load_input_args
8
+ import toshi_hazard_store
9
+
10
+ logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
11
+ logging.getLogger('toshi_hazard_post').setLevel(logging.INFO)
12
+ # logging.getLogger('toshi_hazard_post.aggregation_calc').setLevel(logging.DEBUG)
13
+ # logging.getLogger('toshi_hazard_post.aggregation').setLevel(logging.DEBUG)
14
+ # logging.getLogger('toshi_hazard_post.aggregation_calc').setLevel(logging.DEBUG)
15
+ # logging.getLogger('toshi_hazard_post.logic_tree').setLevel(logging.DEBUG)
16
+ logging.getLogger('toshi_hazard_store').setLevel(logging.ERROR)
17
+ logging.getLogger('toshi_hazard_post').setLevel(logging.INFO)
18
+
19
+
20
+ @click.group()
21
+ def thp():
22
+ pass
23
+
24
+
25
+ @thp.command(name='aggregate', help='aggregate hazard curves')
26
+ @click.argument('input_file', type=click.Path(exists=True))
27
+ def aggregate(input_file):
28
+
29
+ args = load_input_args(input_file)
30
+ click.echo("Toshi Hazard Post: hazard curve aggregation")
31
+ click.echo("=================================================")
32
+ run_aggregation(args)
33
+
34
+
35
+ if __name__ == "__main__":
36
+ thp() # pragma: no cover
scripts/cli_old.py ADDED
@@ -0,0 +1,213 @@
1
+ """Console script for toshi_hazard_post."""
2
+
3
+ import logging
4
+ import os
5
+ import sys
6
+
7
+ import click
8
+ import toml
9
+ from toshi_hazard_store.model import migrate as migrate_ths
10
+
11
+ # from toshi_hazard_post.hazard_aggregation import AggregationConfig, process_aggregation, process_deaggregation
12
+ from toshi_hazard_post.hazard_aggregation.aggregation import process_aggregation
13
+ from toshi_hazard_post.hazard_aggregation.aggregation_config import AggregationConfig
14
+ from toshi_hazard_post.hazard_aggregation.aws_aggregation import distribute_aggregation, push_test_message
15
+ from toshi_hazard_post.hazard_aggregation.aws_deaggregation import distribute_deaggregation
16
+ from toshi_hazard_post.hazard_aggregation.deaggregation import process_deaggregation
17
+ from toshi_hazard_post.hazard_grid.aws_gridded_hazard import distribute_gridded_hazard
18
+ from toshi_hazard_post.hazard_grid.gridded_hazard import calc_gridded_hazard
19
+ from toshi_hazard_post.hazard_grid.misc import get_filter_locations, get_site_lists, migrate
20
+
21
+ log = logging.getLogger()
22
+ logging.basicConfig(level=logging.INFO)
23
+ logging.getLogger('nshm_toshi_client.toshi_client_base').setLevel(logging.INFO)
24
+ logging.getLogger('urllib3').setLevel(logging.INFO)
25
+ logging.getLogger('botocore').setLevel(logging.INFO)
26
+ logging.getLogger('pynamodb').setLevel(logging.INFO)
27
+ logging.getLogger('toshi_hazard_post').setLevel(logging.INFO)
28
+ logging.getLogger('toshi_hazard_store').setLevel(logging.INFO)
29
+ logging.getLogger('gql.transport.requests').setLevel(logging.WARN)
30
+
31
+ formatter = logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
32
+ screen_handler = logging.StreamHandler(stream=sys.stdout)
33
+ screen_handler.setFormatter(formatter)
34
+ log.addHandler(screen_handler)
35
+
36
+
37
+ @click.group()
38
+ def thp():
39
+ pass
40
+
41
+
42
+ @thp.command(name='build-grid', help='calculate hazard grids used to create maps')
43
+ @click.option('-H', '--hazard_model_ids', help='comma-delimted list of hazard model ids.')
44
+ @click.option('-L', '--site-list', help='A site list ENUM.')
45
+ @click.option('-I', '--imts', help='comma-delimited list of imts.')
46
+ @click.option('-A', '--aggs', help='comma-delimited list of aggs.')
47
+ @click.option('-V', '--vs30s', help='comma-delimited list of vs30s.')
48
+ @click.option('-P', '--poes', help='comma-delimited list of poe_levels.')
49
+ @click.option('-c', '--config', type=click.Path(exists=True)) # help="path to a valid THU configuration file."
50
+ @click.option('-lsl', '--list-site-lists', help='print the list of sites list ENUMs and exit', is_flag=True)
51
+ @click.option('-v', '--verbose', is_flag=True)
52
+ @click.option('-d', '--dry-run', is_flag=True)
53
+ @click.option('-m', '--migrate-tables', is_flag=True)
54
+ @click.option('-f', '--force', is_flag=True)
55
+ @click.option(
56
+ '--mode',
57
+ '-m',
58
+ default='LOCAL',
59
+ type=click.Choice(['AWS_BATCH', 'LOCAL'], case_sensitive=True),
60
+ )
61
+ @click.option(
62
+ '--iter-method',
63
+ '-i',
64
+ default='product',
65
+ type=click.Choice(['product', 'zip'], case_sensitive=True),
66
+ )
67
+ @click.option('-w', '--num-workers', default=4, show_default=True)
68
+ def cli_gridded_hazard(
69
+ hazard_model_ids,
70
+ site_list,
71
+ imts,
72
+ aggs,
73
+ vs30s,
74
+ poes,
75
+ config,
76
+ list_site_lists,
77
+ verbose,
78
+ dry_run,
79
+ migrate_tables,
80
+ force,
81
+ mode,
82
+ iter_method,
83
+ num_workers,
84
+ ):
85
+ """Process gridded hazard for a given set of arguments."""
86
+
87
+ if list_site_lists:
88
+ click.echo("ENUM name\tDetails")
89
+ click.echo("===============\t======================================================================")
90
+ for rg in get_site_lists():
91
+ click.echo(f"{rg.name}\t{rg.value}")
92
+ return
93
+
94
+ # site_lists = site_lists.split(',') if site_lists else None
95
+
96
+ hazard_model_ids = hazard_model_ids.split(',') if hazard_model_ids else None
97
+ vs30s = vs30s.split(',') if vs30s else None
98
+ imts = imts.split(',') if imts else None
99
+ aggs = aggs.split(',') if aggs else None
100
+ poes = poes.split(',') if poes else None
101
+ filter_sites = None
102
+
103
+ if config:
104
+ conf = toml.load(config)
105
+ if verbose:
106
+ click.echo(f"using settings in {config} for export")
107
+
108
+ site_list = site_list or conf.get('site_list')
109
+ hazard_model_ids = hazard_model_ids or conf.get('hazard_model_ids')
110
+ imts = imts or conf.get('imts')
111
+ vs30s = vs30s or conf.get('vs30s')
112
+ aggs = aggs or conf.get('aggs')
113
+ poes = poes or conf.get('poes')
114
+ filter_sites = filter_sites or conf.get('filter_sites')
115
+
116
+ if verbose:
117
+ click.echo(f"{hazard_model_ids} {imts} {vs30s}")
118
+
119
+ if dry_run:
120
+ click.echo(f"dry-run {site_list} {hazard_model_ids} {imts} {vs30s}")
121
+ return
122
+
123
+ if migrate_tables:
124
+ click.echo("Ensuring that dynamodb tables are available in target region & stage.")
125
+ migrate()
126
+
127
+ if mode == "LOCAL":
128
+ try:
129
+ click.echo(filter_sites)
130
+ filter_locations = get_filter_locations(filter_sites)
131
+ click.echo(filter_locations)
132
+ calc_gridded_hazard(
133
+ location_grid_id=site_list,
134
+ poe_levels=poes,
135
+ hazard_model_ids=hazard_model_ids,
136
+ vs30s=vs30s,
137
+ imts=imts,
138
+ aggs=aggs,
139
+ num_workers=num_workers,
140
+ force=force,
141
+ filter_locations=filter_locations,
142
+ iter_method=iter_method,
143
+ )
144
+ except Exception as err:
145
+ click.echo(err)
146
+ raise click.UsageError('An error occurred, pls check usage.')
147
+
148
+ # haggs = query_v3.get_hazard_curves(locations, vs30s, hazard_model_ids, imts=imts, aggs=aggs)
149
+ click.echo('Done!')
150
+ elif mode == "AWS_BATCH":
151
+ try:
152
+ click.echo(filter_sites)
153
+ filter_locations = get_filter_locations(filter_sites)
154
+ click.echo(filter_locations)
155
+ distribute_gridded_hazard(
156
+ location_grid_id=site_list,
157
+ poe_levels=poes,
158
+ hazard_model_ids=hazard_model_ids,
159
+ vs30s=vs30s,
160
+ imts=imts,
161
+ aggs=aggs,
162
+ force=force,
163
+ filter_locations=filter_locations,
164
+ iter_method=iter_method,
165
+ )
166
+ except Exception as err:
167
+ click.echo(err)
168
+ raise click.UsageError('An error occurred, pls check usage.')
169
+
170
+
171
+ @thp.command(name='aggregate', help='aggregate hazard curves or disaggregations')
172
+ @click.option(
173
+ '--mode',
174
+ '-m',
175
+ default=lambda: os.environ.get("NZSHM22_THP_MODE", 'LOCAL'),
176
+ type=click.Choice(['AWS', 'AWS_BATCH', 'LOCAL'], case_sensitive=True),
177
+ )
178
+ @click.option('--deagg', '-d', is_flag=True)
179
+ @click.option('--push-sns-test', '-pt', is_flag=True)
180
+ @click.option('--migrate-tables', '-M', is_flag=True)
181
+ @click.argument('config', type=click.Path(exists=True)) # help="path to a valid THP configuration file."
182
+ def aggregate(config, mode, deagg, push_sns_test, migrate_tables):
183
+ """Main entrypoint."""
184
+ click.echo("Hazard post-processing pipeline as serverless AWS infrastructure.")
185
+ click.echo(f"mode: {mode}")
186
+
187
+ agconf = AggregationConfig(config)
188
+ # click.echo(agconf)
189
+ # log.info("Doit")
190
+
191
+ if push_sns_test:
192
+ push_test_message()
193
+ return
194
+
195
+ if mode == 'LOCAL':
196
+ if deagg:
197
+ process_deaggregation(agconf)
198
+ else:
199
+ process_aggregation(agconf)
200
+ return
201
+ if 'AWS_BATCH' in mode: # TODO: multiple vs30s
202
+ if migrate_tables:
203
+ click.echo("Ensuring that dynamodb tables are available in target region & stage.")
204
+ migrate_ths()
205
+ if deagg:
206
+ distribute_deaggregation(agconf, mode)
207
+ else:
208
+ distribute_aggregation(agconf, mode)
209
+ return
210
+
211
+
212
+ if __name__ == "__main__":
213
+ thp() # pragma: no cover
@@ -0,0 +1,5 @@
1
+ # This is the AWS CONTAINER SCRIPT
2
+
3
+ cd /app
4
+ python3 -m ${PYTHON_TASK_MODULE} ${TASK_CONFIG_JSON_QUOTED}
5
+ #END_OF_SCRIPT
@@ -0,0 +1,6 @@
1
+ """This is the top-level package for toshi_hazard_post.
2
+
3
+ This application calculates seismic hazard from pre-calculated realizations.
4
+ """
5
+
6
+ __version__ = '0.7.1'
@@ -0,0 +1,174 @@
1
+ """Module for coordinating and launching aggregation jobs."""
2
+
3
+ import itertools
4
+ import logging
5
+ import sys
6
+ import time
7
+ from concurrent.futures import Executor, ProcessPoolExecutor, as_completed
8
+ from multiprocessing import shared_memory
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING, Generator, Optional
11
+
12
+ import numpy as np
13
+ import pyarrow.orc as orc
14
+ from nzshm_common.location.coded_location import bin_locations
15
+
16
+ import toshi_hazard_post.constants as constants
17
+ from toshi_hazard_post.aggregation_args import AggregationArgs
18
+ from toshi_hazard_post.aggregation_calc import AggSharedArgs, AggTaskArgs, calc_aggregation
19
+ from toshi_hazard_post.aggregation_setup import Site, get_logic_trees, get_sites
20
+ from toshi_hazard_post.data import get_batch_table, get_job_datatable, get_realizations_dataset
21
+ from toshi_hazard_post.local_config import NUM_WORKERS, WORKING_DIR
22
+ from toshi_hazard_post.logic_tree import HazardLogicTree
23
+
24
+ if TYPE_CHECKING:
25
+ import numpy.typing as npt
26
+ from nzshm_common.location import CodedLocation
27
+
28
+ from toshi_hazard_post.logic_tree import HazardComponentBranch
29
+
30
+
31
+ log = logging.getLogger(__name__)
32
+
33
+ PARTITION_RESOLUTION = 1.0
34
+
35
+
36
+ def _generate_agg_jobs(
37
+ sites: list[Site],
38
+ imts: list[str],
39
+ compatibility_key: str,
40
+ component_branches: list['HazardComponentBranch'],
41
+ ) -> Generator[tuple[int, 'CodedLocation', str, Path], None, None]:
42
+
43
+ gmms_digests = [branch.gmcm_hash_digest for branch in component_branches]
44
+ sources_digests = [branch.source_hash_digest for branch in component_branches]
45
+ n_expected = len(component_branches)
46
+
47
+ # group locations by vs30
48
+ vs30s_unique = set([site.vs30 for site in sites])
49
+
50
+ log.info("creating batches from %s sites and %s vs30s" % (len(sites), len(vs30s_unique)))
51
+ for vs30 in vs30s_unique:
52
+ locations = [site.location for site in sites if site.vs30 == vs30]
53
+ location_bins = bin_locations(locations, PARTITION_RESOLUTION)
54
+ for nloc_0, location_bin in location_bins.items():
55
+ dataset = get_realizations_dataset(vs30, nloc_0)
56
+ log.info("batch %d, %s" % (vs30, nloc_0))
57
+ batch_datatable = get_batch_table(
58
+ dataset, compatibility_key, sources_digests, gmms_digests, vs30, nloc_0, imts
59
+ )
60
+ for location, imt in itertools.product(location_bin.locations, imts):
61
+ job_datatable = get_job_datatable(batch_datatable, location, imt, n_expected)
62
+ filepath = Path(WORKING_DIR) / f"{vs30}_{nloc_0}_{location.downsample(0.001).code}_{imt}_dataset.dat"
63
+ log.debug("writing file %s for agg job %s, %s" % (filepath, location.code, imt))
64
+ t0 = time.perf_counter()
65
+ orc.write_table(job_datatable, filepath, compression='snappy')
66
+ t1 = time.perf_counter()
67
+ log.debug("time to write data: %0.5f seconds" % (t1 - t0))
68
+ yield vs30, location, imt, filepath
69
+
70
+
71
+ def run_aggregation(args: AggregationArgs, pool_executor: Optional[Executor] = None) -> None:
72
+ """Main entry point for running aggregation caculations.
73
+
74
+ Args:
75
+ args: the aggregation configuration
76
+ pool_executor: the Executor (e.g. ProcessPoolExecutor) to use for launching the parallel tasks. If
77
+ none given, ProcessPoolExecutor(max_workers=NUM_WORKERS) is used
78
+ """
79
+ time0 = time.perf_counter()
80
+ # get the sites
81
+ log.info("getting sites . . .")
82
+ sites = get_sites(args.site_params.locations_file, args.site_params.locations, args.site_params.vs30s)
83
+ srm_logic_tree, gmcm_logic_tree = get_logic_trees(
84
+ args.hazard_model.nshm_model_version,
85
+ args.hazard_model.srm_logic_tree,
86
+ args.hazard_model.gmcm_logic_tree,
87
+ )
88
+
89
+ # create the logic tree objects and build the full logic tree
90
+ log.info("getting logic trees . . . ")
91
+ logic_tree = HazardLogicTree(srm_logic_tree, gmcm_logic_tree)
92
+ component_branches = logic_tree.component_branches
93
+
94
+ if not args.debug.restart:
95
+ log.info("calculating weights and branch hash table . . . ")
96
+ tic = time.perf_counter()
97
+ weights = logic_tree.weights
98
+ branch_hash_table: list | 'npt.NDArray' = logic_tree.branch_hash_table
99
+ toc = time.perf_counter()
100
+
101
+ log.info('time to build weight array and hash table %0.2f seconds' % (toc - tic))
102
+ log.info("Size of weight array: {}MB".format(weights.nbytes >> 20))
103
+ log.info("Size of hash table: {}MB".format(sys.getsizeof(branch_hash_table) >> 20))
104
+ else:
105
+ branch_hash_table = np.load(args.debug.restart[0])
106
+ weights = np.load(args.debug.restart[1])
107
+
108
+ agg_types = [a.value for a in args.calculation.agg_types]
109
+ imts = [i.value for i in args.calculation.imts]
110
+
111
+ weights_shm = shared_memory.SharedMemory(name=constants.WEIGHTS_SHM_NAME, create=True, size=weights.nbytes)
112
+ branch_hash_table = np.array(branch_hash_table)
113
+ branch_hash_table_shm = shared_memory.SharedMemory(
114
+ name=constants.BRANCH_HASH_TABLE_SHM_NAME, create=True, size=branch_hash_table.nbytes
115
+ )
116
+
117
+ bht: 'npt.NDArray' = np.ndarray(
118
+ branch_hash_table.shape, dtype=branch_hash_table.dtype, buffer=branch_hash_table_shm.buf
119
+ )
120
+ bht[:] = branch_hash_table[:]
121
+ wgt: 'npt.NDArray' = np.ndarray(weights.shape, dtype=weights.dtype, buffer=weights_shm.buf)
122
+ wgt[:] = weights[:]
123
+
124
+ shared_args = AggSharedArgs(
125
+ weights_shape=weights.shape,
126
+ branch_hash_table_shape=branch_hash_table.shape,
127
+ agg_types=agg_types,
128
+ hazard_model_id=args.general.hazard_model_id,
129
+ compatibility_key=args.general.compatibility_key,
130
+ skip_save=args.debug.skip_save,
131
+ )
132
+
133
+ time_parallel_start = time.perf_counter()
134
+ num_jobs = 0
135
+ log.info("starting %d calculations with %d workers" % (len(sites) * len(imts), NUM_WORKERS))
136
+
137
+ futures = {}
138
+ # ds1 = get_realizations_dataset()
139
+ pool_executor = pool_executor or ProcessPoolExecutor(max_workers=NUM_WORKERS)
140
+ with pool_executor as executor:
141
+ for vs30, location, imt, filepath in _generate_agg_jobs(
142
+ sites,
143
+ imts,
144
+ args.general.compatibility_key,
145
+ component_branches,
146
+ # ds1,
147
+ ):
148
+ task_args = AggTaskArgs(
149
+ location=location,
150
+ vs30=vs30,
151
+ imt=imt,
152
+ table_filepath=filepath,
153
+ )
154
+ future = executor.submit(calc_aggregation, task_args, shared_args)
155
+ futures[future] = task_args
156
+ num_jobs += 1
157
+
158
+ num_failed = 0
159
+ for future in as_completed(futures.keys()):
160
+ if exception := future.exception():
161
+ num_failed += 1
162
+ log.error("Exception encountered for task args %s: %s" % (futures[future], repr(exception)))
163
+
164
+ time_parallel_end = time.perf_counter()
165
+ branch_hash_table_shm.close()
166
+ branch_hash_table_shm.unlink()
167
+ weights_shm.close()
168
+ weights_shm.unlink()
169
+
170
+ time1 = time.perf_counter()
171
+ log.info("total time: processed %d calculations in %0.3f seconds" % (num_jobs, time1 - time0))
172
+ log.info("time to perform aggregations after job setup %0.3f" % (time_parallel_end - time_parallel_start))
173
+
174
+ print(f"THERE ARE {num_failed} FAILED JOBS . . . ")
@@ -0,0 +1,215 @@
1
+ """This module defines pydantic classes and functions for the aggregation calculation configuration.
2
+
3
+ Members:
4
+ AggregationArgs: pydantic class for aggregation configuration arguments.
5
+ load_input_args: function to return an AggregationArgs object built from a toml file.
6
+ """
7
+
8
+ import csv
9
+ from collections import namedtuple
10
+ from pathlib import Path
11
+ from typing import Any, Optional, Union
12
+
13
+ import tomlkit
14
+ from nzshm_model import all_model_versions
15
+ from pydantic import AfterValidator, BaseModel, FilePath, PositiveInt, ValidationInfo, field_validator, model_validator
16
+ from toshi_hazard_store.model.constraints import AggregationEnum, IntensityMeasureTypeEnum
17
+ from toshi_hazard_store.scripts.ths_import import chc_manager
18
+ from typing_extensions import Annotated, Self
19
+
20
+
21
+ def load_input_args(filepath: Union[str, Path]) -> 'AggregationArgs':
22
+ """Load the input arguments of a hazard aggregation calculation.
23
+
24
+ Args:
25
+ filepath: the path to the configuration toml file.
26
+
27
+ Returns:
28
+ The aggregation arguments.
29
+ """
30
+ config = tomlkit.parse(Path(filepath).read_text()).unwrap()
31
+ config['filepath'] = Path(filepath)
32
+ return AggregationArgs(**config)
33
+
34
+
35
+ def _resolve_path(path: Union[Path, str], reference_filepath: Union[Path, str]) -> str:
36
+ path = Path(path)
37
+ if not path.is_absolute():
38
+ return str(Path(reference_filepath).parent / path)
39
+ return str(path)
40
+
41
+
42
+ def _is_model_version(value: str) -> str:
43
+ if value not in all_model_versions():
44
+ raise ValueError("must specify valid nshm_model_version ({})".format(all_model_versions()))
45
+ return value
46
+
47
+
48
+ def _is_compat_calc_id(compat_calc_id: str) -> str:
49
+ try:
50
+ chc_manager.load(compat_calc_id)
51
+ except FileNotFoundError:
52
+ raise ValueError("Compatible Hazard Calculation with unique ID {value} does not exist.")
53
+
54
+ return compat_calc_id
55
+
56
+
57
+ class GeneralArgs(BaseModel):
58
+ """The general parameters of a hazard model.
59
+
60
+ Attributes:
61
+ compatibility_key: the toshi-hazard-store compatibility key
62
+ hazard_model_id: the name of the hazard model to use when storing result.
63
+ """
64
+
65
+ compatibility_key: Annotated[str, AfterValidator(_is_compat_calc_id)]
66
+ hazard_model_id: str
67
+
68
+
69
+ class HazardModelArgs(BaseModel):
70
+ """The PSHA hazard model parameters.
71
+
72
+ Logic tree filepaths will override nshm_model_version logic trees.
73
+
74
+ Attributes:
75
+ nshm_model_version: the nzshm-model model version.
76
+ srm_logic_tree: the source logic tree filepath.
77
+ gmcm_logic_tree: the ground motion logic tree filepath.
78
+
79
+ """
80
+
81
+ nshm_model_version: Annotated[Optional[str], AfterValidator(_is_model_version)] = None
82
+ srm_logic_tree: Optional[FilePath] = None
83
+ gmcm_logic_tree: Optional[FilePath] = None
84
+
85
+ @model_validator(mode='after')
86
+ def _check_logic_trees(self) -> Self:
87
+ if not self.nshm_model_version and not (self.srm_logic_tree and self.gmcm_logic_tree):
88
+ raise ValueError(
89
+ """if nshm_model_version not specified, must provide both
90
+ gmcm_logic_tree and srm_logic_tree"""
91
+ )
92
+ return self
93
+
94
+
95
+ class SiteArgs(BaseModel):
96
+ """Site parameters.
97
+
98
+ Restrictions:
99
+ - Must provide one of locations or locations_file but not both.
100
+ - vs30s must be provided as an attribute or in the locations file but not both.
101
+ - if vs30s are provided, they are treated as uniform for all sites.
102
+
103
+ Attributes:
104
+ vs30s:
105
+ locations:
106
+ locations_file:
107
+ """
108
+
109
+ vs30s: Optional[list[PositiveInt]] = None
110
+ locations: Optional[list[str]] = None
111
+ locations_file: Optional[FilePath] = None
112
+
113
+ @staticmethod
114
+ def _has_vs30(filepath: Path):
115
+ with filepath.open() as lf:
116
+ header = lf.readline()
117
+ if "vs30" in header:
118
+ return True
119
+ return False
120
+
121
+ @field_validator('locations_file', mode='after')
122
+ @classmethod
123
+ def _check_file_vs30s(cls, value: FilePath) -> FilePath:
124
+ with value.open() as loc_file:
125
+ site_reader = csv.reader(loc_file)
126
+ Site = namedtuple("Site", next(site_reader), rename=True) # type:ignore
127
+ if 'vs30' in Site._fields:
128
+ for row in site_reader:
129
+ site = Site(*row)
130
+ try:
131
+ vs30 = int(site.vs30) # type:ignore
132
+ assert vs30 > 0
133
+ except ValueError:
134
+ raise ValueError("not all vs30 values are valid {}".format(row))
135
+ return value
136
+
137
+ @model_validator(mode='after')
138
+ def _check_locations(self) -> Self:
139
+ if self.locations_file and self.locations:
140
+ raise ValueError("cannot specify both locations and locations_file")
141
+
142
+ if (not self.locations) and (not self.locations_file):
143
+ raise ValueError("must specify locations or locations_file")
144
+
145
+ file_has_vs30 = self.locations_file and self._has_vs30(self.locations_file)
146
+ if file_has_vs30 and self.vs30s:
147
+ raise ValueError("cannot specify both uniform and site-specific vs30s")
148
+ elif not file_has_vs30 and not self.vs30s:
149
+ raise ValueError("locations file must have vs30 column if uniform vs30s not provided")
150
+
151
+ return self
152
+
153
+
154
+ class CalculationArgs(BaseModel):
155
+ """The calculation parameters.
156
+
157
+ Attributes:
158
+ imts: the intensity measure types.
159
+ agg_types: the aggregation types.
160
+ """
161
+
162
+ imts: list[IntensityMeasureTypeEnum] = [e for e in IntensityMeasureTypeEnum]
163
+ agg_types: list[AggregationEnum] = [e for e in AggregationEnum]
164
+
165
+
166
+ class DebugArgs(BaseModel):
167
+ """The debugging parameters.
168
+
169
+ Attributes:
170
+ skip_save: set to True to skip saving the aggregations.
171
+ restart: tuple of the paths to branch_hash_table and weights numpy files used to quickly start a
172
+ calculation without needing to rebuild the logic tree.
173
+ """
174
+
175
+ skip_save: bool = False
176
+ restart: Optional[tuple[FilePath, FilePath]] = None
177
+
178
+
179
+ class AggregationArgs(BaseModel):
180
+ """The arguments needed to setup a suite of aggregation calculations.
181
+
182
+ Attributes:
183
+ filepath: the location of the config file, used to resolve relative paths.
184
+ general: the general arguments.
185
+ hazard_model: the arguments for the hazard model specification.
186
+ site_params: the site parameters.
187
+ calculation: the calculation parameters.
188
+ debug: the debugging parameters.
189
+ """
190
+
191
+ filepath: FilePath
192
+ general: GeneralArgs
193
+ hazard_model: HazardModelArgs
194
+ site_params: SiteArgs
195
+ calculation: CalculationArgs
196
+ debug: DebugArgs = DebugArgs()
197
+
198
+ # resolve absolute paths (relative to input file) for optional logic tree and config fields
199
+ @field_validator('hazard_model', mode='before')
200
+ @classmethod
201
+ def _absolute_model_paths(cls, data: Any, info: ValidationInfo) -> Any:
202
+ if isinstance(data, dict):
203
+ for key in ["srm_logic_tree", "gmcm_logic_tree"]:
204
+ if data.get(key):
205
+ data[key] = _resolve_path(data[key], info.data["filepath"])
206
+ return data
207
+
208
+ # resolve absolute paths (relative to input file) for optional site file
209
+ @field_validator('site_params', mode='before')
210
+ @classmethod
211
+ def _absolute_site_path(cls, data: Any, info: ValidationInfo) -> Any:
212
+ if isinstance(data, dict):
213
+ if data.get("locations_file"):
214
+ data["locations_file"] = _resolve_path(data["locations_file"], info.data["filepath"])
215
+ return data