1616from ngraph .profiling import PerformanceProfiler , PerformanceReporter
1717from ngraph .report import ReportGenerator
1818from ngraph .scenario import Scenario
19+ from ngraph .utils .output_paths import (
20+ build_artifact_path ,
21+ ensure_parent_dir ,
22+ profiles_dir_for_run ,
23+ resolve_override_path ,
24+ results_path_for_run ,
25+ )
1926
2027logger = get_logger (__name__ )
2128
@@ -1143,19 +1150,21 @@ def _summarize_node_matches(
11431150
11441151def _run_scenario (
11451152 path : Path ,
1146- output : Optional [Path ],
1153+ results_override : Optional [Path ],
11471154 no_results : bool ,
11481155 stdout : bool ,
11491156 keys : Optional [list [str ]] = None ,
11501157 profile : bool = False ,
11511158 profile_memory : bool = False ,
1159+ output_dir : Optional [Path ] = None ,
11521160) -> None :
11531161 """Run a scenario file and export results as JSON by default.
11541162
11551163 Args:
11561164 path: Scenario YAML file.
11571165 output: Optional explicit path where JSON results should be written. When
1158- ``None``, defaults to ``<scenario_name>.json`` in the current directory.
1166+ ``None``, defaults to ``<scenario_name>.results.json`` in the current directory,
1167+ or under ``--output`` if provided.
11591168 no_results: Whether to disable results file generation.
11601169 stdout: Whether to also print results to stdout.
11611170 keys: Optional list of workflow step names to include. When ``None`` all steps are
@@ -1179,8 +1188,8 @@ def _run_scenario(
11791188 logger .info ("Starting scenario execution with profiling" )
11801189
11811190 # Enable child-process profiling for parallel workflows
1182- child_profile_dir = Path ( "worker_profiles" )
1183- child_profile_dir .mkdir (exist_ok = True )
1191+ child_profile_dir = profiles_dir_for_run ( path , output_dir )
1192+ child_profile_dir .mkdir (parents = True , exist_ok = True )
11841193 os .environ ["NGRAPH_PROFILE_DIR" ] = str (child_profile_dir .resolve ())
11851194 logger .info (f"Worker profiles will be saved to: { child_profile_dir } " )
11861195
@@ -1214,10 +1223,13 @@ def _run_scenario(
12141223 f .unlink ()
12151224 except Exception :
12161225 pass
1217- try :
1218- child_profile_dir .rmdir () # Remove dir if empty
1219- except Exception :
1220- pass
1226+ # Keep the profiles directory when an explicit output dir is used
1227+ # to make artifact paths consistent and discoverable.
1228+ if output_dir is None :
1229+ try :
1230+ child_profile_dir .rmdir () # Remove dir if empty
1231+ except Exception :
1232+ pass
12211233
12221234 # Generate and display performance report
12231235 reporter = PerformanceReporter (profiler .results )
@@ -1244,9 +1256,14 @@ def _run_scenario(
12441256
12451257 json_str = json .dumps (results_dict , indent = 2 , default = str )
12461258
1247- # Derive default results file name from scenario when not provided
1248- effective_output = output or Path (f"{ path .stem } .json" )
1259+ # Derive default results file path using output directory policy
1260+ effective_output = results_path_for_run (
1261+ scenario_path = path ,
1262+ output_dir = output_dir ,
1263+ results_override = results_override ,
1264+ )
12491265
1266+ ensure_parent_dir (effective_output )
12501267 logger .info (f"Writing results to: { effective_output } " )
12511268 effective_output .write_text (json_str )
12521269 logger .info ("Results written successfully" )
@@ -1313,7 +1330,8 @@ def main(argv: Optional[List[str]] = None) -> None:
13131330 type = Path ,
13141331 default = None ,
13151332 help = (
1316- "Export results to JSON file (default: <scenario_name>.json in current directory)"
1333+ "Export results to JSON file (default: <scenario_name>.results.json;"
1334+ " placed under --output when provided)"
13171335 ),
13181336 )
13191337 run_parser .add_argument (
@@ -1371,7 +1389,8 @@ def main(argv: Optional[List[str]] = None) -> None:
13711389 "-n" ,
13721390 type = Path ,
13731391 help = (
1374- "Output path for Jupyter notebook (default: <results_name>.ipynb in current directory)"
1392+ "Output path for Jupyter notebook (default: <results_name>.ipynb;"
1393+ " placed under --output when provided)"
13751394 ),
13761395 )
13771396 report_parser .add_argument (
@@ -1380,7 +1399,8 @@ def main(argv: Optional[List[str]] = None) -> None:
13801399 nargs = "?" ,
13811400 const = Path ("analysis.html" ),
13821401 help = (
1383- "Generate HTML report (default: <results_name>.html in current directory if no path specified)"
1402+ "Generate HTML report (default: <results_name>.html if no path specified;"
1403+ " placed under --output when provided)"
13841404 ),
13851405 )
13861406 report_parser .add_argument (
@@ -1389,6 +1409,20 @@ def main(argv: Optional[List[str]] = None) -> None:
13891409 help = "Include code cells in HTML output (default: report without code)" ,
13901410 )
13911411
1412+ # Global output directory for all commands
1413+ for p in (run_parser , inspect_parser , report_parser ):
1414+ p .add_argument (
1415+ "--output" ,
1416+ "-o" ,
1417+ type = Path ,
1418+ default = None ,
1419+ help = (
1420+ "Output directory for generated artifacts. When provided,"
1421+ " all files will be written under this folder using a"
1422+ " consistent '<prefix>.<suffix>' naming convention."
1423+ ),
1424+ )
1425+
13921426 # Determine effective arguments (support both direct calls and module entrypoint)
13931427 effective_args = sys .argv [1 :] if argv is None else argv
13941428
@@ -1410,22 +1444,24 @@ def main(argv: Optional[List[str]] = None) -> None:
14101444
14111445 if args .command == "run" :
14121446 _run_scenario (
1413- args .scenario ,
1414- args .results ,
1415- args .no_results ,
1416- args .stdout ,
1417- args .keys ,
1418- args .profile ,
1419- args .profile_memory ,
1447+ path = args .scenario ,
1448+ results_override = args .results ,
1449+ no_results = args .no_results ,
1450+ stdout = args .stdout ,
1451+ keys = args .keys ,
1452+ profile = args .profile ,
1453+ profile_memory = args .profile_memory ,
1454+ output_dir = args .output ,
14201455 )
14211456 elif args .command == "inspect" :
14221457 _inspect_scenario (args .scenario , args .detail )
14231458 elif args .command == "report" :
14241459 _generate_report (
1425- args .results ,
1426- args .notebook ,
1427- args .html ,
1428- args .include_code ,
1460+ results_path = args .results ,
1461+ notebook_path = args .notebook ,
1462+ html_path = args .html ,
1463+ include_code = args .include_code ,
1464+ output_dir = args .output ,
14291465 )
14301466
14311467
@@ -1434,6 +1470,7 @@ def _generate_report(
14341470 notebook_path : Optional [Path ],
14351471 html_path : Optional [Path ],
14361472 include_code : bool ,
1473+ output_dir : Optional [Path ] = None ,
14371474) -> None :
14381475 """Generate analysis reports from results file.
14391476
@@ -1450,20 +1487,28 @@ def _generate_report(
14501487 generator = ReportGenerator (results_path )
14511488 generator .load_results ()
14521489
1453- # Generate notebook (default derives from results file name)
1454- notebook_output = notebook_path or Path (f"{ results_path .stem } .ipynb" )
1455- generated_notebook = generator .generate_notebook (notebook_output )
1490+ # Determine notebook output path
1491+ nb_out = resolve_override_path (notebook_path , output_dir )
1492+ if nb_out is None :
1493+ nb_out = build_artifact_path (output_dir , results_path .stem , ".ipynb" )
1494+
1495+ ensure_parent_dir (nb_out )
1496+ generated_notebook = generator .generate_notebook (nb_out )
14561497 print (f"✅ Notebook generated: { generated_notebook } " )
14571498
14581499 # Generate HTML if requested
1459- if html_path :
1460- # If --html was passed without an explicit path, argparse provides the const
1461- # value. In that case, derive the HTML name from the results file stem.
1500+ html_out : Optional [Path ] = None
1501+ if html_path is not None :
14621502 if html_path == Path ("analysis.html" ):
1463- html_path = Path (f"{ results_path .stem } .html" )
1503+ html_out = build_artifact_path (output_dir , results_path .stem , ".html" )
1504+ else :
1505+ html_out = resolve_override_path (html_path , output_dir )
1506+
1507+ if html_out is not None :
1508+ ensure_parent_dir (html_out )
14641509 generated_html = generator .generate_html_report (
14651510 notebook_path = generated_notebook ,
1466- html_path = html_path ,
1511+ html_path = html_out ,
14671512 include_code = include_code ,
14681513 )
14691514 print (f"✅ HTML report generated: { generated_html } " )
0 commit comments