Skip to content

HmsMet

Meteorologic model operations for HEC-HMS.

hms_commander.HmsMet

HmsMet - Meteorologic Model File Operations

This module provides static methods for reading and modifying HEC-HMS meteorologic model files (.met). It handles precipitation methods, evapotranspiration, and gage assignments.

All methods are static and designed to be used without instantiation.

HmsMet

Meteorologic model file operations (.met files).

Configure precipitation, evapotranspiration settings, and gage assignments.

All methods are static - no instantiation required.

Example

from hms_commander import HmsMet precip_method = HmsMet.get_precipitation_method("model.met") gage_assignments = HmsMet.get_gage_assignments("model.met")

Source code in hms_commander/HmsMet.py
 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
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
class HmsMet:
    """
    Meteorologic model file operations (.met files).

    Configure precipitation, evapotranspiration settings, and gage assignments.

    All methods are static - no instantiation required.

    Example:
        >>> from hms_commander import HmsMet
        >>> precip_method = HmsMet.get_precipitation_method("model.met")
        >>> gage_assignments = HmsMet.get_gage_assignments("model.met")
    """

    # Meteorologic method enumerations (from _constants)

    @staticmethod
    @log_call
    def get_mets(
        hms_object=None
    ) -> pd.DataFrame:
        """
        Get all meteorologic models from the HMS project.

        Args:
            hms_object: HmsPrj instance (uses global hms if None)

        Returns:
            DataFrame with meteorologic model information
        """
        from .HmsPrj import hms
        hms_obj = hms_object or hms

        if hms_obj is None or not hms_obj.initialized:
            raise RuntimeError("HMS project not initialized")

        return hms_obj.met_df.copy()

    @staticmethod
    @log_call
    def get_precipitation_method(
        met_path: Union[str, Path],
        hms_object=None
    ) -> str:
        """
        Get the precipitation method from a meteorologic model file.

        Args:
            met_path: Path to the .met file
            hms_object: Optional HmsPrj instance

        Returns:
            Precipitation method name string

        Example:
            >>> method = HmsMet.get_precipitation_method("model.met")
            >>> print(f"Method: {method}")
        """
        met_path = Path(met_path)
        content = HmsMet._read_met_file(met_path)
        params = HmsMet._parse_meteorology_block(content)

        return params.get('Precip', 'None')

    @staticmethod
    @log_call
    def get_evapotranspiration_method(
        met_path: Union[str, Path],
        hms_object=None
    ) -> str:
        """
        Get the evapotranspiration method from a meteorologic model file.

        Args:
            met_path: Path to the .met file
            hms_object: Optional HmsPrj instance

        Returns:
            Evapotranspiration method name string
        """
        met_path = Path(met_path)
        content = HmsMet._read_met_file(met_path)
        params = HmsMet._parse_meteorology_block(content)

        return params.get('Evapotranspiration', 'None')

    @staticmethod
    @log_call
    def get_gage_assignments(
        met_path: Union[str, Path],
        hms_object=None
    ) -> pd.DataFrame:
        """
        Get precipitation gage assignments for all subbasins.

        Args:
            met_path: Path to the .met file
            hms_object: Optional HmsPrj instance

        Returns:
            DataFrame with columns: subbasin, precip_gage, weight

        Example:
            >>> assignments = HmsMet.get_gage_assignments("model.met")
            >>> print(assignments)
        """
        met_path = Path(met_path)
        logger.info(f"Reading gage assignments from: {met_path}")

        content = HmsMet._read_met_file(met_path)
        subbasin_blocks = HmsMet._parse_subbasin_blocks(content)

        records = []
        for subbasin_name, attrs in subbasin_blocks.items():
            weight_value = attrs.get('Weight', '1.0')
            record = {
                'subbasin': subbasin_name,
                'precip_gage': attrs.get('Precip Gage'),
                'weight': HmsFileParser.to_numeric(weight_value) if weight_value is not None else 1.0,
            }
            records.append(record)

        df = pd.DataFrame(records)
        logger.info(f"Found {len(df)} gage assignments")
        return df

    @staticmethod
    @log_call
    def set_gage_assignment(
        met_path: Union[str, Path],
        subbasin_name: str,
        gage_name: str,
        weight: float = 1.0,
        hms_object=None
    ) -> bool:
        """
        Set the precipitation gage assignment for a subbasin.

        Args:
            met_path: Path to the .met file
            subbasin_name: Name of the subbasin
            gage_name: Name of the precipitation gage
            weight: Gage weight (default 1.0)
            hms_object: Optional HmsPrj instance

        Returns:
            True if successful

        Example:
            >>> HmsMet.set_gage_assignment("model.met", "Subbasin-1", "Gage-1")
        """
        met_path = Path(met_path)
        content = HmsMet._read_met_file(met_path)

        # Find the subbasin block in the met file
        pattern = rf'(Subbasin:\s*{re.escape(subbasin_name)}\s*\n)(.*?)(End:)'
        match = re.search(pattern, content, re.DOTALL | re.IGNORECASE)

        if match:
            # Update existing block
            block_content = match.group(2)
            block_content = HmsMet._update_param(block_content, 'Precip Gage', gage_name)

            new_block = match.group(1) + block_content + match.group(3)
            content = content[:match.start()] + new_block + content[match.end():]
        else:
            # Add new subbasin block before the final End: of the Meteorology block
            new_block = f"""
Subbasin: {subbasin_name}
     Precip Gage: {gage_name}
End:
"""
            # Find the Meteorology End: and insert before it
            met_end_pattern = r'(Meteorology:.*?)(End:\s*$)'
            content = re.sub(
                met_end_pattern,
                rf'\1{new_block}\2',
                content,
                flags=re.DOTALL | re.MULTILINE
            )

        with open(met_path, 'w', encoding='utf-8') as f:
            f.write(content)

        logger.info(f"Set gage '{gage_name}' for subbasin '{subbasin_name}'")
        return True

    @staticmethod
    @log_call
    def set_all_gage_assignments(
        met_path: Union[str, Path],
        assignments_df: pd.DataFrame,
        create_backup: bool = True,
        hms_object=None
    ) -> Dict:
        """
        Set precipitation gage assignments for multiple subbasins from a DataFrame.

        The DataFrame should have columns: subbasin, precip_gage, and optionally
        weight. Only rows where precip_gage is not NaN are updated.

        Args:
            met_path: Path to the .met file
            assignments_df: DataFrame with columns: subbasin, precip_gage, weight
            create_backup: Create .bak backup before writing (default True)
            hms_object: Optional HmsPrj instance

        Returns:
            Summary dict with keys: subbasins_modified, subbasins_not_found,
            warnings, backup_path

        Example:
            >>> df = HmsMet.get_gage_assignments("model.met")
            >>> df['precip_gage'] = 'New_Gage'  # Reassign all
            >>> result = HmsMet.set_all_gage_assignments("model.met", df)
        """
        import shutil

        met_path = Path(met_path)
        content = HmsMet._read_met_file(met_path)

        summary = {
            'subbasins_modified': 0,
            'subbasins_not_found': [],
            'warnings': [],
            'backup_path': None,
        }

        if 'subbasin' not in assignments_df.columns:
            raise ValueError("assignments_df must have a 'subbasin' column")
        if 'precip_gage' not in assignments_df.columns:
            raise ValueError("assignments_df must have a 'precip_gage' column")

        # Create backup
        if create_backup:
            backup_path = met_path.with_suffix('.met.bak')
            shutil.copy2(met_path, backup_path)
            summary['backup_path'] = str(backup_path)
            logger.info(f"Created backup: {backup_path}")

        # Build lookup: subbasin_name -> {precip_gage, weight}
        update_lookup = {}
        for _, row in assignments_df.iterrows():
            name = row['subbasin']
            gage = row.get('precip_gage')
            if pd.notna(gage):
                update_lookup[name] = {
                    'Precip Gage': str(gage),
                }
                weight = row.get('weight')
                if pd.notna(weight):
                    update_lookup[name]['Weight'] = str(weight)

        # Find all Subbasin blocks with positions
        blocks = HmsFileParser.find_all_blocks(content, "Subbasin")

        found_names = set()

        # Iterate in reverse order to preserve offsets
        for match, name, attrs in reversed(blocks):
            if name not in update_lookup:
                continue

            found_names.add(name)
            updates = update_lookup[name]
            block_body = match.group(3)
            modified = False

            for param_key, new_value in updates.items():
                updated_body, changed = HmsFileParser.update_parameter(
                    block_body, param_key, new_value
                )
                if changed:
                    block_body = updated_body
                    modified = True
                elif param_key not in attrs:
                    # Parameter doesn't exist yet — insert it
                    block_body = block_body + f"     {param_key}: {new_value}\n"
                    modified = True

            if modified:
                header = match.group(1)
                footer = match.group(4)
                new_block = header + block_body + footer
                content = content[:match.start()] + new_block + content[match.end():]
                summary['subbasins_modified'] += 1

        # Check for names not found
        for name in update_lookup:
            if name not in found_names:
                summary['subbasins_not_found'].append(name)

        if summary['subbasins_not_found']:
            summary['warnings'].append(
                f"{len(summary['subbasins_not_found'])} subbasins not found: "
                f"{summary['subbasins_not_found'][:5]}"
            )

        HmsFileParser.write_file(met_path, content)
        logger.info(f"Updated {summary['subbasins_modified']} gage assignments in {met_path.name}")

        return summary

    @staticmethod
    @log_call
    def get_dss_references(
        met_path: Union[str, Path],
        hms_object=None
    ) -> List[Dict[str, str]]:
        """
        Get all DSS file references from a meteorologic model.

        Args:
            met_path: Path to the .met file
            hms_object: Optional HmsPrj instance

        Returns:
            List of dictionaries with DSS file information

        Example:
            >>> dss_refs = HmsMet.get_dss_references("model.met")
            >>> for ref in dss_refs:
            ...     print(f"File: {ref['dss_file']}, Path: {ref['dss_pathname']}")
        """
        met_path = Path(met_path)
        content = HmsMet._read_met_file(met_path)

        dss_refs = []

        # Look for DSS File and DSS Pathname entries
        dss_file_pattern = r'DSS File Name:\s*(.+)'
        dss_path_pattern = r'DSS Pathname:\s*(.+)'

        dss_files = re.findall(dss_file_pattern, content)
        dss_paths = re.findall(dss_path_pattern, content)

        # Pair them up
        for i, dss_file in enumerate(dss_files):
            ref = {
                'dss_file': dss_file.strip(),
                'dss_pathname': dss_paths[i].strip() if i < len(dss_paths) else ''
            }
            dss_refs.append(ref)

        return dss_refs

    @staticmethod
    @log_call
    def get_met_info(
        met_path: Union[str, Path],
        hms_object=None
    ) -> Dict[str, Any]:
        """
        Get comprehensive information from a meteorologic model file.

        Args:
            met_path: Path to the .met file
            hms_object: Optional HmsPrj instance

        Returns:
            Dictionary with all meteorologic model parameters
        """
        met_path = Path(met_path)
        content = HmsMet._read_met_file(met_path)

        met_params = HmsMet._parse_meteorology_block(content)
        subbasin_blocks = HmsMet._parse_subbasin_blocks(content)
        dss_refs = HmsMet.get_dss_references(met_path)

        return {
            'meteorology': met_params,
            'subbasin_assignments': subbasin_blocks,
            'dss_references': dss_refs,
            'num_subbasins': len(subbasin_blocks)
        }

    @staticmethod
    @log_call
    def clone_met(
        template_met: str,
        new_name: str,
        description: str = None,
        hms_object=None
    ) -> Path:
        """
        Clone a meteorologic model file with a new name.

        Follows the CLB Engineering LLM Forward Approach:
        - Non-destructive: Creates new file, preserves original
        - Traceable: Updates description with clone metadata
        - GUI-verifiable: New met model appears in HEC-HMS GUI
        - Project integration: Updates .hms project file

        Args:
            template_met: Name or path of the template met file
            new_name: Name for the new meteorologic model
            description: Optional description (defaults to "Cloned from {template}")
            hms_object: Optional HmsPrj instance

        Returns:
            Path to the new met file

        Raises:
            FileNotFoundError: If template met not found
            FileExistsError: If new met already exists

        Example:
            >>> # Clone for Atlas 14 update
            >>> new_path = HmsMet.clone_met(
            ...     "Design_Storms_TP40",
            ...     "Design_Storms_Atlas14",
            ...     description="Atlas 14 precipitation data",
            ...     hms_object=hms
            ... )
            >>> # New met model now visible in HEC-HMS GUI
        """
        from .HmsUtils import HmsUtils
        from .HmsPrj import hms

        hms_obj = hms_object or hms
        template_path = Path(template_met)

        # Try to resolve template path from project
        if not template_path.exists() and hms_obj is not None and hms_obj.initialized:
            matching = hms_obj.met_df[
                hms_obj.met_df['name'] == template_met
            ]
            if not matching.empty:
                template_path = Path(matching.iloc[0]['full_path'])
                template_name = matching.iloc[0]['name']
            else:
                # Try with .met extension
                potential = Path(template_met)
                if not potential.suffix:
                    template_path = potential.with_suffix('.met')
                    template_name = template_met
                else:
                    template_name = template_path.stem
        else:
            template_name = template_path.stem

        if not template_path.exists():
            raise FileNotFoundError(f"Template met not found: {template_met}")

        # Build new path
        new_path = template_path.parent / f"{new_name}.met"

        # Default description
        if description is None:
            description = f"Cloned from {template_name}"

        # Define modification callback
        def update_met_metadata(lines):
            """Update meteorology name and description in cloned file."""
            modified_lines = []
            in_met_block = False
            description_found = False

            for line in lines:
                # Update Meteorology: line
                if re.match(r'^Meteorology:\s*', line):
                    modified_lines.append(f"Meteorology: {new_name}\n")
                    in_met_block = True
                # Update Description: line if it exists
                elif in_met_block and re.match(r'^\s+Description:\s*', line):
                    modified_lines.append(f"     Description: {description}\n")
                    description_found = True
                # Add Description: if we hit Precip Method or End: without finding one
                elif in_met_block and (re.match(r'^\s+Precip Method:', line) or line.strip() == 'End:'):
                    if not description_found and re.match(r'^\s+Precip Method:', line):
                        # Insert before Precip Method
                        modified_lines.append(f"     Description: {description}\n")
                        description_found = True
                    modified_lines.append(line)
                    if line.strip() == 'End:':
                        in_met_block = False
                        description_found = False
                else:
                    modified_lines.append(line)

            return modified_lines

        # Clone file with modification
        HmsUtils.clone_file(template_path, new_path, update_met_metadata)

        # Update project file if we have an HMS object
        if hms_obj is not None and hms_obj.initialized:
            try:
                HmsUtils.update_project_file(
                    hms_obj.project_file,
                    'Met',
                    new_name
                )

                # Re-initialize to pick up new met
                hms_obj.initialize(hms_obj.project_folder, hms_obj.hms_exe_path)
                logger.info(f"Re-initialized project to register new met '{new_name}'")

            except Exception as e:
                logger.warning(f"Could not update project file: {e}")

        logger.info(f"Cloned met: {template_name} → {new_name}")
        return new_path

    @staticmethod
    @log_call
    def set_precipitation_method(
        met_path: Union[str, Path],
        method: str,
        hms_object=None
    ) -> bool:
        """
        Set the precipitation method in a meteorologic model file.

        Args:
            met_path: Path to the .met file
            method: Precipitation method name
            hms_object: Optional HmsPrj instance

        Returns:
            True if successful
        """
        met_path = Path(met_path)

        if method not in HmsMet.PRECIP_METHODS:
            logger.warning(f"Non-standard precipitation method: {method}")

        content = HmsMet._read_met_file(met_path)
        content = HmsMet._update_param(content, 'Precip', method)

        with open(met_path, 'w', encoding='utf-8') as f:
            f.write(content)

        logger.info(f"Set precipitation method to: {method}")
        return True

    # =========================================================================
    # Private helper methods
    # =========================================================================

    @staticmethod
    def _read_met_file(met_path: Path) -> str:
        """Read met file content with encoding fallback."""
        return HmsFileParser.read_file(met_path)

    @staticmethod
    def _parse_meteorology_block(content: str) -> Dict[str, str]:
        """Parse the main Meteorology block parameters."""
        name, params = HmsFileParser.parse_named_section(content, "Meteorology")
        if name:
            params['name'] = name
        return params

    @staticmethod
    def _parse_subbasin_blocks(content: str) -> Dict[str, Dict[str, str]]:
        """Parse all Subbasin blocks from met file content."""
        return HmsFileParser.parse_blocks(content, "Subbasin")

    @staticmethod
    def _update_param(content: str, param_name: str, new_value: str) -> str:
        """Update a parameter value in met file content."""
        updated, _ = HmsFileParser.update_parameter(content, param_name, new_value)
        return updated

    # =========================================================================
    # Frequency Storm Precipitation Methods (TP40/Atlas 14)
    # =========================================================================

    @staticmethod
    @log_call
    def get_frequency_storm_params(
        met_path: Union[str, Path],
        hms_object=None
    ) -> Dict[str, Any]:
        """
        Get Frequency Based Hypothetical storm parameters from a met file.

        Used for TP40 and Atlas 14 precipitation updates.

        Args:
            met_path: Path to the .met file
            hms_object: Optional HmsPrj instance

        Returns:
            Dictionary with frequency storm parameters including depth values

        Example:
            >>> params = HmsMet.get_frequency_storm_params("1PCT_24HR.met")
            >>> print(f"Duration: {params['total_duration']} min")
            >>> print(f"Depths (inches): {params['depths']}")
        """
        met_path = Path(met_path)
        content = HmsMet._read_met_file(met_path)

        params = {
            'method': None,
            'exceedance_frequency': None,
            'storm_size': None,
            'total_duration': None,
            'time_interval': None,
            'peak_position': None,
            'depths': [],
            'convert_from_annual': False,
            'convert_to_annual': False,
        }

        # Find the Precip Method Parameters block
        pattern = r'Precip Method Parameters:\s*(.+?)\n(.*?)(?=Subbasin:|End:)'
        match = re.search(pattern, content, re.DOTALL)

        if not match:
            logger.warning(f"No Precip Method Parameters block found in {met_path}")
            return params

        params['method'] = match.group(1).strip()
        block = match.group(2)

        for line in block.splitlines():
            line = line.strip()
            if ':' in line:
                key, value = line.split(':', 1)
                key = key.strip()
                value = value.strip()

                if key == 'Exceedence Frequency':
                    params['exceedance_frequency'] = float(value)
                elif key == 'Storm Size':
                    params['storm_size'] = float(value)
                elif key == 'Total Duration':
                    params['total_duration'] = int(value)
                elif key == 'Time Interval':
                    params['time_interval'] = int(value)
                elif key == 'Percent of Duration Before Peak Rainfall':
                    params['peak_position'] = int(value)
                elif key == 'Convert From Annual Series':
                    params['convert_from_annual'] = value.lower() == 'yes'
                elif key == 'Convert to Annual Series':
                    params['convert_to_annual'] = value.lower() == 'yes'
                elif key == 'Depth':
                    try:
                        params['depths'].append(float(value))
                    except ValueError:
                        pass

        logger.info(f"Found {len(params['depths'])} depth values in {met_path.name}")
        return params

    @staticmethod
    @log_call
    def get_precipitation_depths(
        met_path: Union[str, Path],
        hms_object=None
    ) -> List[float]:
        """
        Get precipitation depth values from a frequency storm met file.

        These are the cumulative depth values by duration (e.g., TP40 or Atlas 14).

        Args:
            met_path: Path to the .met file
            hms_object: Optional HmsPrj instance

        Returns:
            List of depth values in inches

        Example:
            >>> depths = HmsMet.get_precipitation_depths("1PCT_24HR.met")
            >>> print(f"24-hr depth: {depths[-1]} inches")
        """
        params = HmsMet.get_frequency_storm_params(met_path, hms_object)
        return params.get('depths', [])

    @staticmethod
    @log_call
    def set_precipitation_depths(
        met_path: Union[str, Path],
        new_depths: List[float],
        hms_object=None
    ) -> bool:
        """
        Set precipitation depth values in a frequency storm met file.

        Used for updating from TP40 to Atlas 14 precipitation values.

        Args:
            met_path: Path to the .met file
            new_depths: List of new depth values in inches
            hms_object: Optional HmsPrj instance

        Returns:
            True if successful

        Raises:
            ValueError: If number of depths doesn't match existing count

        Example:
            >>> # Atlas 14 depths for Houston, 1% AEP, 24-hr
            >>> atlas14_depths = [1.35, 2.4, 4.8, 6.3, 7.4, 9.8, 11.9, 14.5]
            >>> HmsMet.set_precipitation_depths("1PCT_24HR.met", atlas14_depths)
        """
        met_path = Path(met_path)
        content = HmsMet._read_met_file(met_path)

        # Get existing depths to verify count
        existing_params = HmsMet.get_frequency_storm_params(met_path)
        existing_depths = existing_params.get('depths', [])

        if len(new_depths) != len(existing_depths):
            raise ValueError(
                f"New depths count ({len(new_depths)}) must match "
                f"existing count ({len(existing_depths)})"
            )

        # Find and replace depth lines
        depth_pattern = r'^(\s*Depth:\s*)[\d.]+\s*$'
        depth_lines = list(re.finditer(depth_pattern, content, re.MULTILINE))

        if len(depth_lines) != len(new_depths):
            raise ValueError(
                f"Found {len(depth_lines)} depth lines but "
                f"{len(new_depths)} new values provided"
            )

        # Replace in reverse order to preserve positions
        for i, match in enumerate(reversed(depth_lines)):
            idx = len(new_depths) - 1 - i
            new_line = f"     Depth: {new_depths[idx]:.4f}"
            content = content[:match.start()] + new_line + content[match.end():]

        with open(met_path, 'w', encoding='utf-8') as f:
            f.write(content)

        logger.info(f"Updated {len(new_depths)} depth values in {met_path.name}")
        return True

    @staticmethod
    @log_call
    def update_tp40_to_atlas14(
        met_path: Union[str, Path],
        atlas14_depths: List[float],
        hms_object=None
    ) -> Dict[str, Any]:
        """
        Update a met file from TP40 to Atlas 14 precipitation depths.

        This is a convenience method that reads old values, updates to new,
        and returns a summary of changes.

        Args:
            met_path: Path to the .met file
            atlas14_depths: Atlas 14 depth values in inches
            hms_object: Optional HmsPrj instance

        Returns:
            Dictionary with old depths, new depths, and change percentages

        Example:
            >>> atlas14 = [1.35, 2.4, 4.8, 6.3, 7.4, 9.8, 11.9, 14.5]
            >>> result = HmsMet.update_tp40_to_atlas14("1PCT_24HR.met", atlas14)
            >>> print(f"24-hr depth changed by {result['changes'][-1]:.1f}%")
        """
        met_path = Path(met_path)

        # Get original values
        old_depths = HmsMet.get_precipitation_depths(met_path, hms_object)

        if not old_depths:
            raise ValueError(f"No precipitation depths found in {met_path}")

        # Update depths
        HmsMet.set_precipitation_depths(met_path, atlas14_depths, hms_object)

        # Calculate changes
        changes = []
        for old, new in zip(old_depths, atlas14_depths):
            if old > 0:
                pct_change = ((new - old) / old) * 100
            else:
                pct_change = 0 if new == 0 else float('inf')
            changes.append(pct_change)

        result = {
            'met_file': str(met_path),
            'old_depths': old_depths,
            'new_depths': atlas14_depths,
            'changes_percent': changes,
            'avg_change_percent': sum(changes) / len(changes) if changes else 0
        }

        logger.info(
            f"Updated {met_path.name}: avg change {result['avg_change_percent']:.1f}%"
        )
        return result

get_mets(hms_object=None) staticmethod

Get all meteorologic models from the HMS project.

Parameters:

Name Type Description Default
hms_object

HmsPrj instance (uses global hms if None)

None

Returns:

Type Description
DataFrame

DataFrame with meteorologic model information

Source code in hms_commander/HmsMet.py
@staticmethod
@log_call
def get_mets(
    hms_object=None
) -> pd.DataFrame:
    """
    Get all meteorologic models from the HMS project.

    Args:
        hms_object: HmsPrj instance (uses global hms if None)

    Returns:
        DataFrame with meteorologic model information
    """
    from .HmsPrj import hms
    hms_obj = hms_object or hms

    if hms_obj is None or not hms_obj.initialized:
        raise RuntimeError("HMS project not initialized")

    return hms_obj.met_df.copy()

get_precipitation_method(met_path, hms_object=None) staticmethod

Get the precipitation method from a meteorologic model file.

Parameters:

Name Type Description Default
met_path Union[str, Path]

Path to the .met file

required
hms_object

Optional HmsPrj instance

None

Returns:

Type Description
str

Precipitation method name string

Example

method = HmsMet.get_precipitation_method("model.met") print(f"Method: {method}")

Source code in hms_commander/HmsMet.py
@staticmethod
@log_call
def get_precipitation_method(
    met_path: Union[str, Path],
    hms_object=None
) -> str:
    """
    Get the precipitation method from a meteorologic model file.

    Args:
        met_path: Path to the .met file
        hms_object: Optional HmsPrj instance

    Returns:
        Precipitation method name string

    Example:
        >>> method = HmsMet.get_precipitation_method("model.met")
        >>> print(f"Method: {method}")
    """
    met_path = Path(met_path)
    content = HmsMet._read_met_file(met_path)
    params = HmsMet._parse_meteorology_block(content)

    return params.get('Precip', 'None')

get_evapotranspiration_method(met_path, hms_object=None) staticmethod

Get the evapotranspiration method from a meteorologic model file.

Parameters:

Name Type Description Default
met_path Union[str, Path]

Path to the .met file

required
hms_object

Optional HmsPrj instance

None

Returns:

Type Description
str

Evapotranspiration method name string

Source code in hms_commander/HmsMet.py
@staticmethod
@log_call
def get_evapotranspiration_method(
    met_path: Union[str, Path],
    hms_object=None
) -> str:
    """
    Get the evapotranspiration method from a meteorologic model file.

    Args:
        met_path: Path to the .met file
        hms_object: Optional HmsPrj instance

    Returns:
        Evapotranspiration method name string
    """
    met_path = Path(met_path)
    content = HmsMet._read_met_file(met_path)
    params = HmsMet._parse_meteorology_block(content)

    return params.get('Evapotranspiration', 'None')

get_gage_assignments(met_path, hms_object=None) staticmethod

Get precipitation gage assignments for all subbasins.

Parameters:

Name Type Description Default
met_path Union[str, Path]

Path to the .met file

required
hms_object

Optional HmsPrj instance

None

Returns:

Type Description
DataFrame

DataFrame with columns: subbasin, precip_gage, weight

Example

assignments = HmsMet.get_gage_assignments("model.met") print(assignments)

Source code in hms_commander/HmsMet.py
@staticmethod
@log_call
def get_gage_assignments(
    met_path: Union[str, Path],
    hms_object=None
) -> pd.DataFrame:
    """
    Get precipitation gage assignments for all subbasins.

    Args:
        met_path: Path to the .met file
        hms_object: Optional HmsPrj instance

    Returns:
        DataFrame with columns: subbasin, precip_gage, weight

    Example:
        >>> assignments = HmsMet.get_gage_assignments("model.met")
        >>> print(assignments)
    """
    met_path = Path(met_path)
    logger.info(f"Reading gage assignments from: {met_path}")

    content = HmsMet._read_met_file(met_path)
    subbasin_blocks = HmsMet._parse_subbasin_blocks(content)

    records = []
    for subbasin_name, attrs in subbasin_blocks.items():
        weight_value = attrs.get('Weight', '1.0')
        record = {
            'subbasin': subbasin_name,
            'precip_gage': attrs.get('Precip Gage'),
            'weight': HmsFileParser.to_numeric(weight_value) if weight_value is not None else 1.0,
        }
        records.append(record)

    df = pd.DataFrame(records)
    logger.info(f"Found {len(df)} gage assignments")
    return df

set_gage_assignment(met_path, subbasin_name, gage_name, weight=1.0, hms_object=None) staticmethod

Set the precipitation gage assignment for a subbasin.

Parameters:

Name Type Description Default
met_path Union[str, Path]

Path to the .met file

required
subbasin_name str

Name of the subbasin

required
gage_name str

Name of the precipitation gage

required
weight float

Gage weight (default 1.0)

1.0
hms_object

Optional HmsPrj instance

None

Returns:

Type Description
bool

True if successful

Example

HmsMet.set_gage_assignment("model.met", "Subbasin-1", "Gage-1")

Source code in hms_commander/HmsMet.py
    @staticmethod
    @log_call
    def set_gage_assignment(
        met_path: Union[str, Path],
        subbasin_name: str,
        gage_name: str,
        weight: float = 1.0,
        hms_object=None
    ) -> bool:
        """
        Set the precipitation gage assignment for a subbasin.

        Args:
            met_path: Path to the .met file
            subbasin_name: Name of the subbasin
            gage_name: Name of the precipitation gage
            weight: Gage weight (default 1.0)
            hms_object: Optional HmsPrj instance

        Returns:
            True if successful

        Example:
            >>> HmsMet.set_gage_assignment("model.met", "Subbasin-1", "Gage-1")
        """
        met_path = Path(met_path)
        content = HmsMet._read_met_file(met_path)

        # Find the subbasin block in the met file
        pattern = rf'(Subbasin:\s*{re.escape(subbasin_name)}\s*\n)(.*?)(End:)'
        match = re.search(pattern, content, re.DOTALL | re.IGNORECASE)

        if match:
            # Update existing block
            block_content = match.group(2)
            block_content = HmsMet._update_param(block_content, 'Precip Gage', gage_name)

            new_block = match.group(1) + block_content + match.group(3)
            content = content[:match.start()] + new_block + content[match.end():]
        else:
            # Add new subbasin block before the final End: of the Meteorology block
            new_block = f"""
Subbasin: {subbasin_name}
     Precip Gage: {gage_name}
End:
"""
            # Find the Meteorology End: and insert before it
            met_end_pattern = r'(Meteorology:.*?)(End:\s*$)'
            content = re.sub(
                met_end_pattern,
                rf'\1{new_block}\2',
                content,
                flags=re.DOTALL | re.MULTILINE
            )

        with open(met_path, 'w', encoding='utf-8') as f:
            f.write(content)

        logger.info(f"Set gage '{gage_name}' for subbasin '{subbasin_name}'")
        return True

set_all_gage_assignments(met_path, assignments_df, create_backup=True, hms_object=None) staticmethod

Set precipitation gage assignments for multiple subbasins from a DataFrame.

The DataFrame should have columns: subbasin, precip_gage, and optionally weight. Only rows where precip_gage is not NaN are updated.

Parameters:

Name Type Description Default
met_path Union[str, Path]

Path to the .met file

required
assignments_df DataFrame

DataFrame with columns: subbasin, precip_gage, weight

required
create_backup bool

Create .bak backup before writing (default True)

True
hms_object

Optional HmsPrj instance

None

Returns:

Type Description
Dict

Summary dict with keys: subbasins_modified, subbasins_not_found,

Dict

warnings, backup_path

Example

df = HmsMet.get_gage_assignments("model.met") df['precip_gage'] = 'New_Gage' # Reassign all result = HmsMet.set_all_gage_assignments("model.met", df)

Source code in hms_commander/HmsMet.py
@staticmethod
@log_call
def set_all_gage_assignments(
    met_path: Union[str, Path],
    assignments_df: pd.DataFrame,
    create_backup: bool = True,
    hms_object=None
) -> Dict:
    """
    Set precipitation gage assignments for multiple subbasins from a DataFrame.

    The DataFrame should have columns: subbasin, precip_gage, and optionally
    weight. Only rows where precip_gage is not NaN are updated.

    Args:
        met_path: Path to the .met file
        assignments_df: DataFrame with columns: subbasin, precip_gage, weight
        create_backup: Create .bak backup before writing (default True)
        hms_object: Optional HmsPrj instance

    Returns:
        Summary dict with keys: subbasins_modified, subbasins_not_found,
        warnings, backup_path

    Example:
        >>> df = HmsMet.get_gage_assignments("model.met")
        >>> df['precip_gage'] = 'New_Gage'  # Reassign all
        >>> result = HmsMet.set_all_gage_assignments("model.met", df)
    """
    import shutil

    met_path = Path(met_path)
    content = HmsMet._read_met_file(met_path)

    summary = {
        'subbasins_modified': 0,
        'subbasins_not_found': [],
        'warnings': [],
        'backup_path': None,
    }

    if 'subbasin' not in assignments_df.columns:
        raise ValueError("assignments_df must have a 'subbasin' column")
    if 'precip_gage' not in assignments_df.columns:
        raise ValueError("assignments_df must have a 'precip_gage' column")

    # Create backup
    if create_backup:
        backup_path = met_path.with_suffix('.met.bak')
        shutil.copy2(met_path, backup_path)
        summary['backup_path'] = str(backup_path)
        logger.info(f"Created backup: {backup_path}")

    # Build lookup: subbasin_name -> {precip_gage, weight}
    update_lookup = {}
    for _, row in assignments_df.iterrows():
        name = row['subbasin']
        gage = row.get('precip_gage')
        if pd.notna(gage):
            update_lookup[name] = {
                'Precip Gage': str(gage),
            }
            weight = row.get('weight')
            if pd.notna(weight):
                update_lookup[name]['Weight'] = str(weight)

    # Find all Subbasin blocks with positions
    blocks = HmsFileParser.find_all_blocks(content, "Subbasin")

    found_names = set()

    # Iterate in reverse order to preserve offsets
    for match, name, attrs in reversed(blocks):
        if name not in update_lookup:
            continue

        found_names.add(name)
        updates = update_lookup[name]
        block_body = match.group(3)
        modified = False

        for param_key, new_value in updates.items():
            updated_body, changed = HmsFileParser.update_parameter(
                block_body, param_key, new_value
            )
            if changed:
                block_body = updated_body
                modified = True
            elif param_key not in attrs:
                # Parameter doesn't exist yet — insert it
                block_body = block_body + f"     {param_key}: {new_value}\n"
                modified = True

        if modified:
            header = match.group(1)
            footer = match.group(4)
            new_block = header + block_body + footer
            content = content[:match.start()] + new_block + content[match.end():]
            summary['subbasins_modified'] += 1

    # Check for names not found
    for name in update_lookup:
        if name not in found_names:
            summary['subbasins_not_found'].append(name)

    if summary['subbasins_not_found']:
        summary['warnings'].append(
            f"{len(summary['subbasins_not_found'])} subbasins not found: "
            f"{summary['subbasins_not_found'][:5]}"
        )

    HmsFileParser.write_file(met_path, content)
    logger.info(f"Updated {summary['subbasins_modified']} gage assignments in {met_path.name}")

    return summary

get_dss_references(met_path, hms_object=None) staticmethod

Get all DSS file references from a meteorologic model.

Parameters:

Name Type Description Default
met_path Union[str, Path]

Path to the .met file

required
hms_object

Optional HmsPrj instance

None

Returns:

Type Description
List[Dict[str, str]]

List of dictionaries with DSS file information

Example

dss_refs = HmsMet.get_dss_references("model.met") for ref in dss_refs: ... print(f"File: {ref['dss_file']}, Path: {ref['dss_pathname']}")

Source code in hms_commander/HmsMet.py
@staticmethod
@log_call
def get_dss_references(
    met_path: Union[str, Path],
    hms_object=None
) -> List[Dict[str, str]]:
    """
    Get all DSS file references from a meteorologic model.

    Args:
        met_path: Path to the .met file
        hms_object: Optional HmsPrj instance

    Returns:
        List of dictionaries with DSS file information

    Example:
        >>> dss_refs = HmsMet.get_dss_references("model.met")
        >>> for ref in dss_refs:
        ...     print(f"File: {ref['dss_file']}, Path: {ref['dss_pathname']}")
    """
    met_path = Path(met_path)
    content = HmsMet._read_met_file(met_path)

    dss_refs = []

    # Look for DSS File and DSS Pathname entries
    dss_file_pattern = r'DSS File Name:\s*(.+)'
    dss_path_pattern = r'DSS Pathname:\s*(.+)'

    dss_files = re.findall(dss_file_pattern, content)
    dss_paths = re.findall(dss_path_pattern, content)

    # Pair them up
    for i, dss_file in enumerate(dss_files):
        ref = {
            'dss_file': dss_file.strip(),
            'dss_pathname': dss_paths[i].strip() if i < len(dss_paths) else ''
        }
        dss_refs.append(ref)

    return dss_refs

get_met_info(met_path, hms_object=None) staticmethod

Get comprehensive information from a meteorologic model file.

Parameters:

Name Type Description Default
met_path Union[str, Path]

Path to the .met file

required
hms_object

Optional HmsPrj instance

None

Returns:

Type Description
Dict[str, Any]

Dictionary with all meteorologic model parameters

Source code in hms_commander/HmsMet.py
@staticmethod
@log_call
def get_met_info(
    met_path: Union[str, Path],
    hms_object=None
) -> Dict[str, Any]:
    """
    Get comprehensive information from a meteorologic model file.

    Args:
        met_path: Path to the .met file
        hms_object: Optional HmsPrj instance

    Returns:
        Dictionary with all meteorologic model parameters
    """
    met_path = Path(met_path)
    content = HmsMet._read_met_file(met_path)

    met_params = HmsMet._parse_meteorology_block(content)
    subbasin_blocks = HmsMet._parse_subbasin_blocks(content)
    dss_refs = HmsMet.get_dss_references(met_path)

    return {
        'meteorology': met_params,
        'subbasin_assignments': subbasin_blocks,
        'dss_references': dss_refs,
        'num_subbasins': len(subbasin_blocks)
    }

clone_met(template_met, new_name, description=None, hms_object=None) staticmethod

Clone a meteorologic model file with a new name.

Follows the CLB Engineering LLM Forward Approach: - Non-destructive: Creates new file, preserves original - Traceable: Updates description with clone metadata - GUI-verifiable: New met model appears in HEC-HMS GUI - Project integration: Updates .hms project file

Parameters:

Name Type Description Default
template_met str

Name or path of the template met file

required
new_name str

Name for the new meteorologic model

required
description str

Optional description (defaults to "Cloned from {template}")

None
hms_object

Optional HmsPrj instance

None

Returns:

Type Description
Path

Path to the new met file

Raises:

Type Description
FileNotFoundError

If template met not found

FileExistsError

If new met already exists

Example
Clone for Atlas 14 update

new_path = HmsMet.clone_met( ... "Design_Storms_TP40", ... "Design_Storms_Atlas14", ... description="Atlas 14 precipitation data", ... hms_object=hms ... )

New met model now visible in HEC-HMS GUI
Source code in hms_commander/HmsMet.py
@staticmethod
@log_call
def clone_met(
    template_met: str,
    new_name: str,
    description: str = None,
    hms_object=None
) -> Path:
    """
    Clone a meteorologic model file with a new name.

    Follows the CLB Engineering LLM Forward Approach:
    - Non-destructive: Creates new file, preserves original
    - Traceable: Updates description with clone metadata
    - GUI-verifiable: New met model appears in HEC-HMS GUI
    - Project integration: Updates .hms project file

    Args:
        template_met: Name or path of the template met file
        new_name: Name for the new meteorologic model
        description: Optional description (defaults to "Cloned from {template}")
        hms_object: Optional HmsPrj instance

    Returns:
        Path to the new met file

    Raises:
        FileNotFoundError: If template met not found
        FileExistsError: If new met already exists

    Example:
        >>> # Clone for Atlas 14 update
        >>> new_path = HmsMet.clone_met(
        ...     "Design_Storms_TP40",
        ...     "Design_Storms_Atlas14",
        ...     description="Atlas 14 precipitation data",
        ...     hms_object=hms
        ... )
        >>> # New met model now visible in HEC-HMS GUI
    """
    from .HmsUtils import HmsUtils
    from .HmsPrj import hms

    hms_obj = hms_object or hms
    template_path = Path(template_met)

    # Try to resolve template path from project
    if not template_path.exists() and hms_obj is not None and hms_obj.initialized:
        matching = hms_obj.met_df[
            hms_obj.met_df['name'] == template_met
        ]
        if not matching.empty:
            template_path = Path(matching.iloc[0]['full_path'])
            template_name = matching.iloc[0]['name']
        else:
            # Try with .met extension
            potential = Path(template_met)
            if not potential.suffix:
                template_path = potential.with_suffix('.met')
                template_name = template_met
            else:
                template_name = template_path.stem
    else:
        template_name = template_path.stem

    if not template_path.exists():
        raise FileNotFoundError(f"Template met not found: {template_met}")

    # Build new path
    new_path = template_path.parent / f"{new_name}.met"

    # Default description
    if description is None:
        description = f"Cloned from {template_name}"

    # Define modification callback
    def update_met_metadata(lines):
        """Update meteorology name and description in cloned file."""
        modified_lines = []
        in_met_block = False
        description_found = False

        for line in lines:
            # Update Meteorology: line
            if re.match(r'^Meteorology:\s*', line):
                modified_lines.append(f"Meteorology: {new_name}\n")
                in_met_block = True
            # Update Description: line if it exists
            elif in_met_block and re.match(r'^\s+Description:\s*', line):
                modified_lines.append(f"     Description: {description}\n")
                description_found = True
            # Add Description: if we hit Precip Method or End: without finding one
            elif in_met_block and (re.match(r'^\s+Precip Method:', line) or line.strip() == 'End:'):
                if not description_found and re.match(r'^\s+Precip Method:', line):
                    # Insert before Precip Method
                    modified_lines.append(f"     Description: {description}\n")
                    description_found = True
                modified_lines.append(line)
                if line.strip() == 'End:':
                    in_met_block = False
                    description_found = False
            else:
                modified_lines.append(line)

        return modified_lines

    # Clone file with modification
    HmsUtils.clone_file(template_path, new_path, update_met_metadata)

    # Update project file if we have an HMS object
    if hms_obj is not None and hms_obj.initialized:
        try:
            HmsUtils.update_project_file(
                hms_obj.project_file,
                'Met',
                new_name
            )

            # Re-initialize to pick up new met
            hms_obj.initialize(hms_obj.project_folder, hms_obj.hms_exe_path)
            logger.info(f"Re-initialized project to register new met '{new_name}'")

        except Exception as e:
            logger.warning(f"Could not update project file: {e}")

    logger.info(f"Cloned met: {template_name} → {new_name}")
    return new_path

set_precipitation_method(met_path, method, hms_object=None) staticmethod

Set the precipitation method in a meteorologic model file.

Parameters:

Name Type Description Default
met_path Union[str, Path]

Path to the .met file

required
method str

Precipitation method name

required
hms_object

Optional HmsPrj instance

None

Returns:

Type Description
bool

True if successful

Source code in hms_commander/HmsMet.py
@staticmethod
@log_call
def set_precipitation_method(
    met_path: Union[str, Path],
    method: str,
    hms_object=None
) -> bool:
    """
    Set the precipitation method in a meteorologic model file.

    Args:
        met_path: Path to the .met file
        method: Precipitation method name
        hms_object: Optional HmsPrj instance

    Returns:
        True if successful
    """
    met_path = Path(met_path)

    if method not in HmsMet.PRECIP_METHODS:
        logger.warning(f"Non-standard precipitation method: {method}")

    content = HmsMet._read_met_file(met_path)
    content = HmsMet._update_param(content, 'Precip', method)

    with open(met_path, 'w', encoding='utf-8') as f:
        f.write(content)

    logger.info(f"Set precipitation method to: {method}")
    return True

get_frequency_storm_params(met_path, hms_object=None) staticmethod

Get Frequency Based Hypothetical storm parameters from a met file.

Used for TP40 and Atlas 14 precipitation updates.

Parameters:

Name Type Description Default
met_path Union[str, Path]

Path to the .met file

required
hms_object

Optional HmsPrj instance

None

Returns:

Type Description
Dict[str, Any]

Dictionary with frequency storm parameters including depth values

Example

params = HmsMet.get_frequency_storm_params("1PCT_24HR.met") print(f"Duration: {params['total_duration']} min") print(f"Depths (inches): {params['depths']}")

Source code in hms_commander/HmsMet.py
@staticmethod
@log_call
def get_frequency_storm_params(
    met_path: Union[str, Path],
    hms_object=None
) -> Dict[str, Any]:
    """
    Get Frequency Based Hypothetical storm parameters from a met file.

    Used for TP40 and Atlas 14 precipitation updates.

    Args:
        met_path: Path to the .met file
        hms_object: Optional HmsPrj instance

    Returns:
        Dictionary with frequency storm parameters including depth values

    Example:
        >>> params = HmsMet.get_frequency_storm_params("1PCT_24HR.met")
        >>> print(f"Duration: {params['total_duration']} min")
        >>> print(f"Depths (inches): {params['depths']}")
    """
    met_path = Path(met_path)
    content = HmsMet._read_met_file(met_path)

    params = {
        'method': None,
        'exceedance_frequency': None,
        'storm_size': None,
        'total_duration': None,
        'time_interval': None,
        'peak_position': None,
        'depths': [],
        'convert_from_annual': False,
        'convert_to_annual': False,
    }

    # Find the Precip Method Parameters block
    pattern = r'Precip Method Parameters:\s*(.+?)\n(.*?)(?=Subbasin:|End:)'
    match = re.search(pattern, content, re.DOTALL)

    if not match:
        logger.warning(f"No Precip Method Parameters block found in {met_path}")
        return params

    params['method'] = match.group(1).strip()
    block = match.group(2)

    for line in block.splitlines():
        line = line.strip()
        if ':' in line:
            key, value = line.split(':', 1)
            key = key.strip()
            value = value.strip()

            if key == 'Exceedence Frequency':
                params['exceedance_frequency'] = float(value)
            elif key == 'Storm Size':
                params['storm_size'] = float(value)
            elif key == 'Total Duration':
                params['total_duration'] = int(value)
            elif key == 'Time Interval':
                params['time_interval'] = int(value)
            elif key == 'Percent of Duration Before Peak Rainfall':
                params['peak_position'] = int(value)
            elif key == 'Convert From Annual Series':
                params['convert_from_annual'] = value.lower() == 'yes'
            elif key == 'Convert to Annual Series':
                params['convert_to_annual'] = value.lower() == 'yes'
            elif key == 'Depth':
                try:
                    params['depths'].append(float(value))
                except ValueError:
                    pass

    logger.info(f"Found {len(params['depths'])} depth values in {met_path.name}")
    return params

get_precipitation_depths(met_path, hms_object=None) staticmethod

Get precipitation depth values from a frequency storm met file.

These are the cumulative depth values by duration (e.g., TP40 or Atlas 14).

Parameters:

Name Type Description Default
met_path Union[str, Path]

Path to the .met file

required
hms_object

Optional HmsPrj instance

None

Returns:

Type Description
List[float]

List of depth values in inches

Example

depths = HmsMet.get_precipitation_depths("1PCT_24HR.met") print(f"24-hr depth: {depths[-1]} inches")

Source code in hms_commander/HmsMet.py
@staticmethod
@log_call
def get_precipitation_depths(
    met_path: Union[str, Path],
    hms_object=None
) -> List[float]:
    """
    Get precipitation depth values from a frequency storm met file.

    These are the cumulative depth values by duration (e.g., TP40 or Atlas 14).

    Args:
        met_path: Path to the .met file
        hms_object: Optional HmsPrj instance

    Returns:
        List of depth values in inches

    Example:
        >>> depths = HmsMet.get_precipitation_depths("1PCT_24HR.met")
        >>> print(f"24-hr depth: {depths[-1]} inches")
    """
    params = HmsMet.get_frequency_storm_params(met_path, hms_object)
    return params.get('depths', [])

set_precipitation_depths(met_path, new_depths, hms_object=None) staticmethod

Set precipitation depth values in a frequency storm met file.

Used for updating from TP40 to Atlas 14 precipitation values.

Parameters:

Name Type Description Default
met_path Union[str, Path]

Path to the .met file

required
new_depths List[float]

List of new depth values in inches

required
hms_object

Optional HmsPrj instance

None

Returns:

Type Description
bool

True if successful

Raises:

Type Description
ValueError

If number of depths doesn't match existing count

Example
Atlas 14 depths for Houston, 1% AEP, 24-hr

atlas14_depths = [1.35, 2.4, 4.8, 6.3, 7.4, 9.8, 11.9, 14.5] HmsMet.set_precipitation_depths("1PCT_24HR.met", atlas14_depths)

Source code in hms_commander/HmsMet.py
@staticmethod
@log_call
def set_precipitation_depths(
    met_path: Union[str, Path],
    new_depths: List[float],
    hms_object=None
) -> bool:
    """
    Set precipitation depth values in a frequency storm met file.

    Used for updating from TP40 to Atlas 14 precipitation values.

    Args:
        met_path: Path to the .met file
        new_depths: List of new depth values in inches
        hms_object: Optional HmsPrj instance

    Returns:
        True if successful

    Raises:
        ValueError: If number of depths doesn't match existing count

    Example:
        >>> # Atlas 14 depths for Houston, 1% AEP, 24-hr
        >>> atlas14_depths = [1.35, 2.4, 4.8, 6.3, 7.4, 9.8, 11.9, 14.5]
        >>> HmsMet.set_precipitation_depths("1PCT_24HR.met", atlas14_depths)
    """
    met_path = Path(met_path)
    content = HmsMet._read_met_file(met_path)

    # Get existing depths to verify count
    existing_params = HmsMet.get_frequency_storm_params(met_path)
    existing_depths = existing_params.get('depths', [])

    if len(new_depths) != len(existing_depths):
        raise ValueError(
            f"New depths count ({len(new_depths)}) must match "
            f"existing count ({len(existing_depths)})"
        )

    # Find and replace depth lines
    depth_pattern = r'^(\s*Depth:\s*)[\d.]+\s*$'
    depth_lines = list(re.finditer(depth_pattern, content, re.MULTILINE))

    if len(depth_lines) != len(new_depths):
        raise ValueError(
            f"Found {len(depth_lines)} depth lines but "
            f"{len(new_depths)} new values provided"
        )

    # Replace in reverse order to preserve positions
    for i, match in enumerate(reversed(depth_lines)):
        idx = len(new_depths) - 1 - i
        new_line = f"     Depth: {new_depths[idx]:.4f}"
        content = content[:match.start()] + new_line + content[match.end():]

    with open(met_path, 'w', encoding='utf-8') as f:
        f.write(content)

    logger.info(f"Updated {len(new_depths)} depth values in {met_path.name}")
    return True

update_tp40_to_atlas14(met_path, atlas14_depths, hms_object=None) staticmethod

Update a met file from TP40 to Atlas 14 precipitation depths.

This is a convenience method that reads old values, updates to new, and returns a summary of changes.

Parameters:

Name Type Description Default
met_path Union[str, Path]

Path to the .met file

required
atlas14_depths List[float]

Atlas 14 depth values in inches

required
hms_object

Optional HmsPrj instance

None

Returns:

Type Description
Dict[str, Any]

Dictionary with old depths, new depths, and change percentages

Example

atlas14 = [1.35, 2.4, 4.8, 6.3, 7.4, 9.8, 11.9, 14.5] result = HmsMet.update_tp40_to_atlas14("1PCT_24HR.met", atlas14) print(f"24-hr depth changed by {result['changes'][-1]:.1f}%")

Source code in hms_commander/HmsMet.py
@staticmethod
@log_call
def update_tp40_to_atlas14(
    met_path: Union[str, Path],
    atlas14_depths: List[float],
    hms_object=None
) -> Dict[str, Any]:
    """
    Update a met file from TP40 to Atlas 14 precipitation depths.

    This is a convenience method that reads old values, updates to new,
    and returns a summary of changes.

    Args:
        met_path: Path to the .met file
        atlas14_depths: Atlas 14 depth values in inches
        hms_object: Optional HmsPrj instance

    Returns:
        Dictionary with old depths, new depths, and change percentages

    Example:
        >>> atlas14 = [1.35, 2.4, 4.8, 6.3, 7.4, 9.8, 11.9, 14.5]
        >>> result = HmsMet.update_tp40_to_atlas14("1PCT_24HR.met", atlas14)
        >>> print(f"24-hr depth changed by {result['changes'][-1]:.1f}%")
    """
    met_path = Path(met_path)

    # Get original values
    old_depths = HmsMet.get_precipitation_depths(met_path, hms_object)

    if not old_depths:
        raise ValueError(f"No precipitation depths found in {met_path}")

    # Update depths
    HmsMet.set_precipitation_depths(met_path, atlas14_depths, hms_object)

    # Calculate changes
    changes = []
    for old, new in zip(old_depths, atlas14_depths):
        if old > 0:
            pct_change = ((new - old) / old) * 100
        else:
            pct_change = 0 if new == 0 else float('inf')
        changes.append(pct_change)

    result = {
        'met_file': str(met_path),
        'old_depths': old_depths,
        'new_depths': atlas14_depths,
        'changes_percent': changes,
        'avg_change_percent': sum(changes) / len(changes) if changes else 0
    }

    logger.info(
        f"Updated {met_path.name}: avg change {result['avg_change_percent']:.1f}%"
    )
    return result
CLB Engineering Corporation  ·  LLM Forward Engineering
HMS Commander is a free and open-source project maintained by CLB Engineering Corporation. For agencies and firms seeking to modernize H&H workflows with LLM Forward approaches, contact CLB to partner with the engineers who wrote the automation.