99from schimpy .small_areas import small_areas
1010from schimpy .split_quad import split_quad
1111from schimpy import schism_yaml
12+ from schimpy import station as station_mod
1213from schimpy .create_vgrid_lsc2 import vgrid_gen
1314from schimpy .mesh_volume_tvd import (
1415 refine_volume_tvd ,
1516 ShorelineOptions ,
1617 FloorOptions ,
1718 TVDOptions ,
1819)
20+ import dms_datastore .dstore_config as configs
1921from packaging .version import Version , InvalidVersion
2022import datetime
2123import importlib
@@ -456,6 +458,109 @@ def create_fluxflag(s, inputs, logger):
456458 buf = "{}\n " .format (line ["name" ])
457459 f .write (buf )
458460
461+ def _validate_station_request (request , logger ):
462+ """Return a normalized request list validated against station_mod.station_variables."""
463+ allowed = set (station_mod .station_variables ) # elev, salt, etc.
464+ if isinstance (request , str ):
465+ if request .lower () == "all" :
466+ return "all"
467+ request_list = [request ]
468+ else :
469+ request_list = list (request )
470+ # allow 'all' anywhere
471+ if any (str (x ).lower () == "all" for x in request_list ):
472+ return "all"
473+ bad = [x for x in request_list if x not in allowed ]
474+ if bad :
475+ logger .error (f"station_output.request contains unknown variables: { bad } . Allowed: { sorted (allowed )} " )
476+ raise ValueError ("Invalid station_output.request" )
477+ return request_list
478+
479+ def create_station_output (inputs , logger ):
480+ """Create/aggregate station.in from the new top-level 'station_output' section."""
481+ section = inputs .get ("station_output" )
482+ if section is None :
483+ return
484+ # Accept either key (common misspelling safeguard)
485+ station_in_list = section .get ("station_in_files" )
486+
487+ name = section .get ("name" , "station.in" )
488+ request = _validate_station_request (section .get ("request" , "all" ), logger )
489+ out_fpath = ensure_outdir (inputs ["prepro_output_dir" ], name )
490+
491+ # If ANY entry is 'GENERATE' (case-insensitive), create a temp file from DBs,
492+ # then (optionally) concatenate with any additional provided files.
493+ generated_tmp = None
494+ if station_in_list is not None and any (str (x ).strip ().upper () == "GENERATE" for x in station_in_list ):
495+ logger .info ("station_output: GENERATE requested (will merge with other files if provided)" )
496+ station_db = configs .config_file ("station_dbase" )
497+ subloc_db = configs .config_file ("sublocations" )
498+ if station_db is None or subloc_db is None :
499+ raise ValueError (
500+ "station_output: Could not resolve default station databases. "
501+ "Check dms_datastore config for 'station_dbase' and 'sublocations'."
502+ )
503+ # Write to a temp file inside prepro_output_dir so we can include it in the concat
504+ generated_tmp = ensure_outdir (inputs ["prepro_output_dir" ], "__station_generated.in" )
505+ logger .info (f"station_output: GENERATE -> writing temp { generated_tmp } " )
506+ station_mod .convert_db_station_in (
507+ outfile = generated_tmp ,
508+ stationdb = station_db ,
509+ sublocdb = subloc_db ,
510+ station_request = request ,
511+ default = - 0.5 ,
512+ )
513+
514+ # Mode 2: concatenate listed station.in files
515+ import pandas as pd
516+ dfs = []
517+ if not station_in_list :
518+ raise ValueError ("station_output.station_in_files must be provided (or include 'GENERATE')." )
519+ # Build the effective list: generated tmp (if any) + all explicit files except the 'GENERATE' token
520+ files_to_read = []
521+ if generated_tmp is not None :
522+ files_to_read .append (generated_tmp )
523+ files_to_read .extend ([f for f in station_in_list if str (f ).strip ().upper () != "GENERATE" ])
524+
525+ for f in files_to_read :
526+ f = os .path .expanduser (str (f ))
527+ if not os .path .isabs (f ):
528+ # allow relative to the launch YAML directory or CWD—leave as-is
529+ pass
530+ if not os .path .exists (f ):
531+ logger .error (f"station_output: cannot find file '{ f } '" )
532+ raise ValueError (f"station_output file not found: { f } " )
533+ logger .info (f"station_output: reading { f } " )
534+ dfs .append (station_mod .read_station_in (f ))
535+
536+ if not dfs :
537+ raise ValueError ("station_output: no station files to merge." )
538+
539+ merged = pd .concat (dfs )
540+ # Strict check: fail if any (id, subloc) appears more than once
541+ dup_mask = merged .index .duplicated (keep = False )
542+ if dup_mask .any ():
543+ # Summarize the first few duplicates for a helpful error
544+ dup_keys = merged .index [dup_mask ]
545+ # unique but preserve order
546+ seen = set ()
547+ uniq_dups = [k for k in dup_keys if not (k in seen or seen .add (k ))]
548+ sample = ", " .join ([f"{ k [0 ]} ::{ k [1 ]} " for k in uniq_dups [:10 ]])
549+ more = "" if len (uniq_dups ) <= 10 else f" (+{ len (uniq_dups )- 10 } more)"
550+ raise ValueError (
551+ "station_output: duplicate (station, sublocation) entries detected; "
552+ f"please remove redundancy. Examples: { sample } { more } "
553+ )
554+
555+ # Write final station.in with the requested variables; write_station_in handles row renumbering
556+ logger .info (f"station_output: writing merged file -> { out_fpath } " )
557+ station_mod .write_station_in (out_fpath , merged , request = request )
558+ # Clean up generated temp if we made one
559+ if generated_tmp is not None :
560+ try :
561+ os .remove (generated_tmp )
562+ except Exception :
563+ logger .warning (f"station_output: could not remove temp file { generated_tmp } " )
459564
460565def update_spatial_inputs (s , inputs , logger ):
461566 """Create SCHISM grid inputs.
@@ -474,6 +579,7 @@ def update_spatial_inputs(s, inputs, logger):
474579 create_prop_with_polygons (s , inputs , logger )
475580 create_structures (s , inputs , logger )
476581 create_fluxflag (s , inputs , logger )
582+ create_station_output (inputs , logger )
477583
478584
479585def update_temporal_inputs (s , inputs ):
@@ -608,6 +714,7 @@ def process_prepare_yaml(in_fname, use_logging=True):
608714 "hydraulics" ,
609715 "sources_sinks" ,
610716 "flow_outputs" ,
717+ "station_output" ,
611718 "copy_resources" ,
612719 ] + schism_yaml .include_keywords
613720 logger .info ("Processing the top level..." )
@@ -659,8 +766,9 @@ def prepare_schism(args, use_logging=True):
659766 if item_exist (inputs , "copy_resources" ):
660767 logger .info ("Copying resources to output dir" )
661768 copy_spec = inputs ["copy_resources" ]
769+ prepro_outdir = inputs ["prepro_output_dir" ]
662770 for key , item in copy_spec .items ():
663- outpath = os . path . join ( outdir , item )
771+ outpath = ensure_outdir ( prepro_outdir , item )
664772 if os .path .normpath (key ) == os .path .normpath (outpath ):
665773 continue
666774 logger .info (f"copy_resources: copying { key } to { outpath } " )
0 commit comments