From 9466c4e06200f32bb69664acb3716ff28470153f Mon Sep 17 00:00:00 2001 From: rcbrost Date: Fri, 5 Dec 2025 10:55:20 -0700 Subject: [PATCH 01/29] Exploring command-line arguments and experiment_settings.ini files. Rough rough example_scene_reconstruction.py working from the command line. --- example/enclosed_energy/enclosed_energy.ipynb | 6 +- .../example_scene_reconstruction.py | 134 ++++++++++++++++-- opencsp/common/lib/tool/log_tools.py | 2 +- 3 files changed, 127 insertions(+), 15 deletions(-) diff --git a/example/enclosed_energy/enclosed_energy.ipynb b/example/enclosed_energy/enclosed_energy.ipynb index fc185049f..f29f83410 100644 --- a/example/enclosed_energy/enclosed_energy.ipynb +++ b/example/enclosed_energy/enclosed_energy.ipynb @@ -729,9 +729,9 @@ ], "metadata": { "kernelspec": { - "display_name": "venv-3.11", + "display_name": "env_310_OpenCSP", "language": "python", - "name": "venv-3.11" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -743,7 +743,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.10.9" } }, "nbformat": 4, diff --git a/example/scene_reconstruction/example_scene_reconstruction.py b/example/scene_reconstruction/example_scene_reconstruction.py index 96528df81..61a4b561e 100644 --- a/example/scene_reconstruction/example_scene_reconstruction.py +++ b/example/scene_reconstruction/example_scene_reconstruction.py @@ -10,14 +10,12 @@ import opencsp.common.lib.tool.log_tools as lt -def scene_reconstruction(dir_output, dir_input): +def scene_reconstruction(dir_input, dir_output): """ Reconstructs the XYZ locations of Aruco markers in a scene. Parameters ---------- - dir_output : str - The directory where the output files, including point locations and calibration figures, will be saved. dir_input : str The directory containing the input files needed for scene reconstruction. This includes: @@ -26,6 +24,8 @@ def scene_reconstruction(dir_output, dir_input): - 'aruco_marker_images/NAME.JPG': Directory containing images of Aruco markers. - 'point_pair_distances.csv': CSV file with distances between point pairs. - 'alignment_points.csv': CSV file with alignment points. + dir_output : str + The directory where the output files, including point locations and calibration figures, will be saved. Notes ----- @@ -42,7 +42,7 @@ def scene_reconstruction(dir_output, dir_input): Examples -------- - >>> scene_reconstruction('/path/to/output', '/path/to/input') + >>> scene_reconstruction('/path/to/input', '/path/to/output') """ # "ChatGPT 4o" assisted with generating this docstring. @@ -74,11 +74,25 @@ def scene_reconstruction(dir_output, dir_input): # Save calibration figures for fig in cal_scene_recon.figures: - fig.savefig(join(dir_output, fig.get_label() + '.png')) + figure_path_body = join(dir_output, fig.get_label() + '.png') + lt.info('before overwrite check, figure_path_body = ' + figure_path_body) + # Overwrite previous versions. + if ft.file_exists(figure_path_body): + ft.delete_file(figure_path_body) + fig.savefig(figure_path_body) -def example_driver(dir_output_fixture, dir_input_fixture): +def example_scene_reconstruction_driver(dir_input_fixture, dir_output_fixture): + """ + Sets up and runs the scene_reconstruction() routine. + Parameters + ---------- + dir_input_fixture : str + Directory to read input. Called a fixture because it might be provided by pytest. + dir_output_fixture : str + Directory to write output. Called a fixture because it might be provided by pytest. + """ dir_input = join(opencsp_code_dir(), 'app/scene_reconstruction/test/data/data_measurement') dir_output = join(dirname(__file__), 'data/output/scene_reconstruction') if dir_input_fixture: @@ -86,14 +100,112 @@ def example_driver(dir_output_fixture, dir_input_fixture): if dir_output_fixture: dir_output = dir_output_fixture - # Define output directory - ft.create_directories_if_necessary(dir_input) + # Ensure output directory is ready + ft.create_directories_if_necessary(dir_output) # Set up logger - lt.logger(join(dir_output, 'log.txt'), lt.log.INFO) + logfile_dir_body_ext = join(dir_output, 'log.txt') + lt.logger(logfile_dir_body_ext, lt.log.INFO) + lt.info('Starting program ' + __file__) - scene_reconstruction(dir_output, dir_input) + lt.info('dir_input = ' + dir_input) + lt.info('dir_output = ' + dir_output) + lt.info('Calling routine scene_reconstruction(dir_input, dir_output)...') + scene_reconstruction(dir_input, dir_output) if __name__ == '__main__': - example_driver() + # ?? RCB SCAFFOLDING RCB -- DELETE FOLLOWING + import argparse + import configparser + import os + + # Start argparse + # parser = argparse.ArgumentParser(prog=__file__.rstrip(".py"), description="Sensitive strings searcher") + # Source - https://stackoverflow.com/a + # Posted by Martijn Pieters, modified by community. See post 'Timeline' for change history + # Retrieved 2025-12-04, License - CC BY-SA 4.0 + parser = argparse.ArgumentParser( + prog=__file__.rstrip(".py"), + description="Sensitive strings searcher with defaults in help", + # ... other options ... + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "--no-interactive", + action="store_true", + dest="ninteractive", + help="Don't interactively ask the user about unknown binary files. Simply fail instead.", + ) + parser.add_argument( + "--accept-all", + action="store_true", + dest="acceptall", + help="Don't interactively ask the user about unknown binary files. Simply accept all as verified on the user's behalf. " + + "This can be useful when you're confident that the only changes have been that the binary files have moved but not changed.", + ) + parser.add_argument( + "--accept-unfound", + action="store_true", + dest="acceptunfound", + help="Don't fail because of unfound expected binary files. Instead remove the expected files from the list of allowed binaries. " + + "This can be useful when you're confident that the only changes have been that the binary files have moved but not changed.", + ) + parser.add_argument( + "--progress", action="store_true", dest="print_progress", help="Draw the progress while scanning." + ) + parser.add_argument( + "--verbose", + action="store_true", + dest="verbose", + help="Print more information while running. Overrides '--progress'.", + ) + parser.add_argument( + "--base-path", + required=False, + dest="basepath", + default="C:\\ctemp", + help="The directory to open images relative to.", + ) + parser.add_argument("paths", nargs="+", type=str, help="Paths to images") + args = parser.parse_args() + not_interactive: bool = args.ninteractive + accept_all: bool = args.acceptall + remove_unfound_binaries: bool = args.acceptunfound + print_progress: bool = args.print_progress + verbose: bool = args.verbose + basepath: str = args.basepath + paths: list[str] = list(args.paths) + # End argparse + # Begin print argparse + print("not_interactive = ", not_interactive) + print("accept_all = ", accept_all) + print("remove_unfound_binaries = ", remove_unfound_binaries) + print("print_progress = ", print_progress) + print("verbose = ", verbose) + print("basepath = ", basepath) + print("paths = ", paths) + # End print argparse + + print("current_working_directory = ", os.getcwd()) + experiment_settings_file = "DUMMY_experiment_settings.ini" + print("experiment_settings_file = ", experiment_settings_file) + experiment_settings_dir_body_ext = os.path.join(basepath, experiment_settings_file) + print("experiment_settings_dir_body_ext = ", experiment_settings_dir_body_ext) + experiment_settings = configparser.ConfigParser() + # experiment_settings.read(experiment_settings_file) + experiment_settings.read(experiment_settings_dir_body_ext) + print("experiment_settings = ", experiment_settings) + process_dir = experiment_settings["Default"]["process_dir"] + print("process_dir = ", process_dir) + bcs_images_dir = experiment_settings["Default"]["bcs_images_dir"] + print("bcs_images_dir = ", bcs_images_dir) + dir_main_input = experiment_settings["Default"]["dir_input"] + print("dir_main_input = ", dir_main_input) + dir_main_output = experiment_settings["Default"]["dir_output"] + print("dir_main_output = ", dir_main_output) + # assert False + # ?? RCB SCAFFOLDING RCB -- END SCAFFOLDING + # dir_main_input = join(opencsp_code_dir(), 'app/scene_reconstruction/test/data/data_measurement') + # dir_main_output = join(dirname(__file__), 'data/output/scene_reconstruction') + example_scene_reconstruction_driver(dir_main_input, dir_main_output) diff --git a/opencsp/common/lib/tool/log_tools.py b/opencsp/common/lib/tool/log_tools.py index cb8791283..f0f599996 100644 --- a/opencsp/common/lib/tool/log_tools.py +++ b/opencsp/common/lib/tool/log_tools.py @@ -24,7 +24,7 @@ def logger(log_dir_body_ext: str = None, level: int = log.INFO, delete_existing_log: bool = True) -> log.Logger: """Initialize logging for single-process programs. - Creates a fresh log file, deleting the existing log file if it exists as indicated by delete_existing_log_file. + Creates a fresh log file, deleting the existing log file if it exists as indicated by delete_existing_log. Once this method is called, then the debug(), info(), warn(), error(), and critical() methods will use the logger created here. Example usage:: From 54a098f9fe4f75b8fd9bba11252010fd5c811984 Mon Sep 17 00:00:00 2001 From: rcbrost Date: Fri, 5 Dec 2025 16:26:00 -0700 Subject: [PATCH 02/29] Progress in implementing actual command-line arguments and .ini file for example_scene_reconstruction.py --- .../example_scene_reconstruction.py | 103 ++++++------------ 1 file changed, 31 insertions(+), 72 deletions(-) diff --git a/example/scene_reconstruction/example_scene_reconstruction.py b/example/scene_reconstruction/example_scene_reconstruction.py index 61a4b561e..1ef840729 100644 --- a/example/scene_reconstruction/example_scene_reconstruction.py +++ b/example/scene_reconstruction/example_scene_reconstruction.py @@ -127,85 +127,44 @@ def example_scene_reconstruction_driver(dir_input_fixture, dir_output_fixture): # Retrieved 2025-12-04, License - CC BY-SA 4.0 parser = argparse.ArgumentParser( prog=__file__.rstrip(".py"), - description="Sensitive strings searcher with defaults in help", - # ... other options ... + description="Example scene reconstruction calculation. Given photos with Aruco markers, find marker and camera 3-d positions.", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) + # parser.add_argument( + # "--verbose", + # action="store_true", + # dest="verbose", + # help="Print more information while running. Overrides '--progress'.", + # ) parser.add_argument( - "--no-interactive", - action="store_true", - dest="ninteractive", - help="Don't interactively ask the user about unknown binary files. Simply fail instead.", - ) - parser.add_argument( - "--accept-all", - action="store_true", - dest="acceptall", - help="Don't interactively ask the user about unknown binary files. Simply accept all as verified on the user's behalf. " - + "This can be useful when you're confident that the only changes have been that the binary files have moved but not changed.", - ) - parser.add_argument( - "--accept-unfound", - action="store_true", - dest="acceptunfound", - help="Don't fail because of unfound expected binary files. Instead remove the expected files from the list of allowed binaries. " - + "This can be useful when you're confident that the only changes have been that the binary files have moved but not changed.", - ) - parser.add_argument( - "--progress", action="store_true", dest="print_progress", help="Draw the progress while scanning." - ) - parser.add_argument( - "--verbose", - action="store_true", - dest="verbose", - help="Print more information while running. Overrides '--progress'.", - ) - parser.add_argument( - "--base-path", + "-s", + "--settings_dir_body_ext", required=False, - dest="basepath", - default="C:\\ctemp", - help="The directory to open images relative to.", + dest="settings_dir_body_ext", + default=None, + help="The directory root for reading data and writing output for this run.", ) - parser.add_argument("paths", nargs="+", type=str, help="Paths to images") args = parser.parse_args() - not_interactive: bool = args.ninteractive - accept_all: bool = args.acceptall - remove_unfound_binaries: bool = args.acceptunfound - print_progress: bool = args.print_progress - verbose: bool = args.verbose - basepath: str = args.basepath - paths: list[str] = list(args.paths) + settings_dir_body_ext: str = args.settings_dir_body_ext + print("arg: settings_dir_body_ext = ", settings_dir_body_ext) # End argparse - # Begin print argparse - print("not_interactive = ", not_interactive) - print("accept_all = ", accept_all) - print("remove_unfound_binaries = ", remove_unfound_binaries) - print("print_progress = ", print_progress) - print("verbose = ", verbose) - print("basepath = ", basepath) - print("paths = ", paths) - # End print argparse - - print("current_working_directory = ", os.getcwd()) - experiment_settings_file = "DUMMY_experiment_settings.ini" - print("experiment_settings_file = ", experiment_settings_file) - experiment_settings_dir_body_ext = os.path.join(basepath, experiment_settings_file) - print("experiment_settings_dir_body_ext = ", experiment_settings_dir_body_ext) - experiment_settings = configparser.ConfigParser() - # experiment_settings.read(experiment_settings_file) - experiment_settings.read(experiment_settings_dir_body_ext) - print("experiment_settings = ", experiment_settings) - process_dir = experiment_settings["Default"]["process_dir"] - print("process_dir = ", process_dir) - bcs_images_dir = experiment_settings["Default"]["bcs_images_dir"] - print("bcs_images_dir = ", bcs_images_dir) - dir_main_input = experiment_settings["Default"]["dir_input"] - print("dir_main_input = ", dir_main_input) - dir_main_output = experiment_settings["Default"]["dir_output"] - print("dir_main_output = ", dir_main_output) + + if settings_dir_body_ext is None: + dir_main_input = join(opencsp_code_dir(), 'app/scene_reconstruction/test/data/data_measurement') + dir_main_output = join(dirname(__file__), 'data/output/scene_reconstruction') + write_full_data = False + else: + print("current_working_directory = ", os.getcwd()) + print("settings_dir_body_ext = ", settings_dir_body_ext) + print("ft.file_exists(settings_dir_body_ext) = ", ft.file_exists(settings_dir_body_ext)) + settings = configparser.ConfigParser() + settings.read(settings_dir_body_ext) + dir_main_input = settings["Default"]["dir_input"] + dir_main_output = settings["Default"]["dir_output"] + write_full_data = settings["Default"]["write_full_data"] # assert False # ?? RCB SCAFFOLDING RCB -- END SCAFFOLDING - # dir_main_input = join(opencsp_code_dir(), 'app/scene_reconstruction/test/data/data_measurement') - # dir_main_output = join(dirname(__file__), 'data/output/scene_reconstruction') + print("Before calling driver, dir_main_input = ", dir_main_input) + print("Before calling driver, dir_main_output = ", dir_main_output) + print("Before calling driver, write_full_data = ", write_full_data) example_scene_reconstruction_driver(dir_main_input, dir_main_output) From 0b10195b2c2c91c24e878d112c3ef3f2d4c2e9f2 Mon Sep 17 00:00:00 2001 From: rcbrost Date: Sat, 6 Dec 2025 11:45:10 -0700 Subject: [PATCH 03/29] Finished upgrading example_scene_reconstruction.py to accept arguments either for command line or pytest. --- example/conftest.py | 25 ++- .../example_scene_reconstruction.py | 168 +++++++++++++----- 2 files changed, 146 insertions(+), 47 deletions(-) diff --git a/example/conftest.py b/example/conftest.py index 0dfb0fd92..1e06f1ba3 100644 --- a/example/conftest.py +++ b/example/conftest.py @@ -1,19 +1,34 @@ +""" +Setup pytest for running examples. +""" + import pytest # -# Ensure pytest adds root directory to the system path. +# Add pytest command-line arguments supported by examples. # def pytest_addoption(parser): - parser.addoption('--dir-input', action='store', default='', help='Base directory with data input') - parser.addoption('--dir-output', action='store', default='', help='Base directory where output will be written') + parser.addoption('--dir_input', action='store', default='', help='Base directory with data input') + parser.addoption('--dir_output', action='store', default='', help='Base directory where output will be written') + parser.addoption( + '--write_full_data', + action='store', + default='False', + help='If true, write out a directory structure including all input data. Otherwise only write generated output.', + ) @pytest.fixture def dir_input_fixture(request): - return request.config.getoption('--dir-input') + return request.config.getoption('--dir_input') @pytest.fixture def dir_output_fixture(request): - return request.config.getoption('--dir-output') + return request.config.getoption('--dir_output') + + +@pytest.fixture +def write_full_data_fixture(request): + return request.config.getoption('--write_full_data') diff --git a/example/scene_reconstruction/example_scene_reconstruction.py b/example/scene_reconstruction/example_scene_reconstruction.py index 1ef840729..719efc146 100644 --- a/example/scene_reconstruction/example_scene_reconstruction.py +++ b/example/scene_reconstruction/example_scene_reconstruction.py @@ -1,5 +1,87 @@ -from os.path import join, dirname +r"""("r" prefix to ignore escape characters within docstring.) +Exercise scene reconstruction algorithms. + +Supports these use cases: + +1. Pytest execution. + Purpose: To verify code is still operating properly. + + a. Quick pytest unit tests. Run pytest from opencsp directory. + In OpenCSP directory: + pytest + + b. More detailed pytest examples. + In OpenCSP\example directory: + pytest + + c. Automated tests for pull request management. + + d. Automated tests for nightly and weekly function checks. + Run pytest from opencsp/example directory (with arguments to run full-scale data). + + e. Run pytest on only this example. + In OpenCSP\example directory: + pytest .\scene_reconstruction\example_scene_reconstruction.py + + f. Using pytest as the vehicle for full-scale example execution. + In OpenCSP\example directory: + pytest .\scene_reconstruction\example_scene_reconstruction.py --dir_input=C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\data_measurement --dir_output=C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\output --write_full_data=True + + g. Using pytest as the vehicle for example execution on user data. + In OpenCSP\example directory: + pytest .\scene_reconstruction\example_scene_reconstruction.py --dir_input= --dir_output= --write_full_data= + +2. Running the example from the command line. + Purpose: To apply the example calculation to new data. + + a. With no arguments, to execute default behavior (for practice and study). + In OpenCSP\example\scene_reconstruction directory: + python .\example_scene_reconstruction.py + + b. With an argument to read a user-specified settings file to define input sources, output locations, and execution details. + i. Data source/sink options: + (1) Data built into the repository. No argument. + In OpenCSP\example\scene_reconstruction directory: + python .\example_scene_reconstruction.py + (2) Community data on local machine. myfile_ctemp.ini + In OpenCSP\example\scene_reconstruction directory: + python .\example_scene_reconstruction.py --settings_dir_body_ext "C:\ctemp\OpenCSP_ctemp\example_scene_reconstruction_settings_ctemp.ini" + (3) Data on local machine, but in user-owned location. myfile_.ini + In OpenCSP\example\scene_reconstruction directory: + python .\example_scene_reconstruction.py --settings_dir_body_ext "C:\Users\\OpenCSP\OpenCSP_\example_scene_reconstruction_settings_.ini" + (4) General network location. myfile_.ini + In OpenCSP\example\scene_reconstruction directory: + python .\example_scene_reconstruction.py --settings_dir_body_ext "\\\OpenCSP_\example_scene_reconstruction_settings_.ini" + ii. Output levels: + (1) Output only newly computed information. In .ini: write_full_data = False + (2) Output full beginning-to-end data corpus in a linear set of directories. + In .ini: write_full_data = True + + c. Review command-line options: + In OpenCSP\example\scene_reconstruction directory: + python .\example_scene_reconstruction.py --help + +3. Calling the example calculation from other code. + Purpose: To utilize the example calculation within a larger computation or application. + + a. Arbitrary context and general data. + In calling file, import example_scenario_reconstruction, call driver. (??other name??) + +4. Running an example with the Visual Studio Code debugger. + Purpose: To interact with the code execution (break points, check stack variables, etc), either for study or fixing an error. Could apply to either 2 or 3 above. + + a. Use default settings, running on default data. In VS Code, press F5 key. + + b. Temporarily modify internal variable values to test specific data computation. + Edit in VS Code, then press F5 key. + +""" + +from os.path import join, basename, dirname + +import argparse +import configparser import numpy as np from opencsp.app.scene_reconstruction.lib.SceneReconstruction import SceneReconstruction @@ -10,7 +92,7 @@ import opencsp.common.lib.tool.log_tools as lt -def scene_reconstruction(dir_input, dir_output): +def scene_reconstruction(dir_input, dir_output, write_full_data): """ Reconstructs the XYZ locations of Aruco markers in a scene. @@ -26,6 +108,8 @@ def scene_reconstruction(dir_input, dir_output): - 'alignment_points.csv': CSV file with alignment points. dir_output : str The directory where the output files, including point locations and calibration figures, will be saved. + write_full_data : bool + If true, write out a directory structure including all input data. Otherwise only write generated output. Notes ----- @@ -75,30 +159,35 @@ def scene_reconstruction(dir_input, dir_output): # Save calibration figures for fig in cal_scene_recon.figures: figure_path_body = join(dir_output, fig.get_label() + '.png') - lt.info('before overwrite check, figure_path_body = ' + figure_path_body) # Overwrite previous versions. if ft.file_exists(figure_path_body): ft.delete_file(figure_path_body) fig.savefig(figure_path_body) -def example_scene_reconstruction_driver(dir_input_fixture, dir_output_fixture): +def example_scene_reconstruction_driver(dir_input_fixture, dir_output_fixture, write_full_data_fixture): """ Sets up and runs the scene_reconstruction() routine. Parameters ---------- dir_input_fixture : str - Directory to read input. Called a fixture because it might be provided by pytest. + Directory to read input. Has fixture suffix because it might be provided by pytest. dir_output_fixture : str - Directory to write output. Called a fixture because it might be provided by pytest. + Directory to write output. Has fixture suffix because it might be provided by pytest. + fixture_dir_write_full_data : bool + If true, write out a directory structure including all input data. Otherwise only write generated output. + Has fixture suffix because it might be provided by pytest. """ dir_input = join(opencsp_code_dir(), 'app/scene_reconstruction/test/data/data_measurement') dir_output = join(dirname(__file__), 'data/output/scene_reconstruction') + write_full_data = False if dir_input_fixture: dir_input = dir_input_fixture if dir_output_fixture: dir_output = dir_output_fixture + if write_full_data_fixture: + write_full_data = write_full_data_fixture # Ensure output directory is ready ft.create_directories_if_necessary(dir_output) @@ -110,18 +199,15 @@ def example_scene_reconstruction_driver(dir_input_fixture, dir_output_fixture): lt.info('dir_input = ' + dir_input) lt.info('dir_output = ' + dir_output) - lt.info('Calling routine scene_reconstruction(dir_input, dir_output)...') - scene_reconstruction(dir_input, dir_output) + lt.info('write_full_data = ' + str(write_full_data)) + lt.info('Calling routine scene_reconstruction(dir_input, dir_output, write_full_data)...') + scene_reconstruction(dir_input, dir_output, write_full_data) if __name__ == '__main__': - # ?? RCB SCAFFOLDING RCB -- DELETE FOLLOWING - import argparse - import configparser - import os - - # Start argparse - # parser = argparse.ArgumentParser(prog=__file__.rstrip(".py"), description="Sensitive strings searcher") + # Parse command-line arguments, if any. + # Execute "python .py --help" to see usage tips. + # # Source - https://stackoverflow.com/a # Posted by Martijn Pieters, modified by community. See post 'Timeline' for change history # Retrieved 2025-12-04, License - CC BY-SA 4.0 @@ -130,12 +216,6 @@ def example_scene_reconstruction_driver(dir_input_fixture, dir_output_fixture): description="Example scene reconstruction calculation. Given photos with Aruco markers, find marker and camera 3-d positions.", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) - # parser.add_argument( - # "--verbose", - # action="store_true", - # dest="verbose", - # help="Print more information while running. Overrides '--progress'.", - # ) parser.add_argument( "-s", "--settings_dir_body_ext", @@ -145,26 +225,30 @@ def example_scene_reconstruction_driver(dir_input_fixture, dir_output_fixture): help="The directory root for reading data and writing output for this run.", ) args = parser.parse_args() - settings_dir_body_ext: str = args.settings_dir_body_ext - print("arg: settings_dir_body_ext = ", settings_dir_body_ext) - # End argparse - - if settings_dir_body_ext is None: - dir_main_input = join(opencsp_code_dir(), 'app/scene_reconstruction/test/data/data_measurement') - dir_main_output = join(dirname(__file__), 'data/output/scene_reconstruction') - write_full_data = False + arg_settings_dir_body_ext: str = args.settings_dir_body_ext + print("arg: settings_dir_body_ext = ", arg_settings_dir_body_ext) + + # Get settings + if arg_settings_dir_body_ext is None: + print("Using default control settings.") + dir_input_main = join(opencsp_code_dir(), 'app/scene_reconstruction/test/data/data_measurement') + dir_output_main = join(dirname(__file__), 'data/output/scene_reconstruction') + write_full_data_main = False else: - print("current_working_directory = ", os.getcwd()) - print("settings_dir_body_ext = ", settings_dir_body_ext) - print("ft.file_exists(settings_dir_body_ext) = ", ft.file_exists(settings_dir_body_ext)) + print("Loading control from settings file:", arg_settings_dir_body_ext) + if not ft.file_exists(arg_settings_dir_body_ext): + print("ERROR: In " + basename(__file__) + ", settings file does not exist. Settings file:") + print(" ", arg_settings_dir_body_ext) + assert False settings = configparser.ConfigParser() - settings.read(settings_dir_body_ext) - dir_main_input = settings["Default"]["dir_input"] - dir_main_output = settings["Default"]["dir_output"] - write_full_data = settings["Default"]["write_full_data"] - # assert False - # ?? RCB SCAFFOLDING RCB -- END SCAFFOLDING - print("Before calling driver, dir_main_input = ", dir_main_input) - print("Before calling driver, dir_main_output = ", dir_main_output) - print("Before calling driver, write_full_data = ", write_full_data) - example_scene_reconstruction_driver(dir_main_input, dir_main_output) + settings.read(arg_settings_dir_body_ext) + dir_input_main = settings["Default"]["dir_input"] + dir_output_main = settings["Default"]["dir_output"] + write_full_data_main = settings["Default"]["write_full_data"] + + # Call driver, noting status first. + print("Calling driver:") + print(" dir_main_input = ", dir_input_main) + print(" dir_main_output = ", dir_output_main) + print(" write_full_data = ", write_full_data_main) + example_scene_reconstruction_driver(dir_input_main, dir_output_main, write_full_data_main) From 856dfda5b9fbfe4d91a2eeed7b15f2ff8d0d05f1 Mon Sep 17 00:00:00 2001 From: rcbrost Date: Mon, 22 Dec 2025 10:07:19 -0700 Subject: [PATCH 04/29] Documented how to run SelectImagePoints.py --- opencsp/app/select_image_points/README.md | 48 +++++++++++++++++++ .../select_image_points/SelectImagePoints.py | 48 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/opencsp/app/select_image_points/README.md b/opencsp/app/select_image_points/README.md index fa8b63293..8d4a7fd02 100644 --- a/opencsp/app/select_image_points/README.md +++ b/opencsp/app/select_image_points/README.md @@ -3,3 +3,51 @@ Select points on an image by hand and save results to text file. - 'escape' key closes the window and discards the results. - 's' key saves selected points as a text file in the root directory. + +Notes: + 1. When the window launches, it first raises a dialog to select an image file. The dialog box only + shows .RAW or .NEF files. Switch to "All files" to select images of other types (.jpg, .png, etc). + + 2. After selecting an image file, the program diplays the image. But for some reason it shows up + behind all other windows. If you minimize all other windows, then you can see the image. + Note that the display does not include the windows border, or minimize/maximize/close buttons, etc. + + 3. There are no prompts, but the program silently waits for you to click on the image somewhere. + When you do, the image will be replaced by a small tile from the image in the vicinity of where + you clicked, shown highly magnified. This enables you to select individual pixels, while seeing + surrounding context. + + 4. After you make the fine-grain selction on the enlarged window, the progrem redisplays the full + image and waits. + + 5. You can then repeat the process to select additional points. The program is logging your selections + in the background. + + 6. When you are done selecting points, press "s" to save and exit. There is no confirmation raised + or written to the console. + + 7. The program writes the selected points in a file "points_.txt", which is written to + the directory from which you launched the program. The file contains the path to the selected image. + + 8. Thus, to control the location where the list of points is written, cd to the directory you wish to + save to, and then launch the program. You can use the file selection dialog to navigate to the + image you want to select. + + For example: + (env_310_OpenCSP) PS C:\> cd C:\ctemp\select_image_points_test\ + (env_310_OpenCSP) PS C:\ctemp\select_image_points_test> python C:\\Code\OpenCSP\opencsp\app\select_image_points\SelectImagePoints.py + + The above will work if you have your default python run environment set to the virtual environment + env_310_OpenCSP. If not, then you should explicitly specify this to ensure that all of the OpenCSP + packages are avaialble. You can do this by: + PS C:\ctemp\select_image_points_test> C:\\Code\env_310_OpenCSP\Scripts\python.exe C:\\Code\OpenCSP\opencsp\app\select_image_points\SelectImagePoints.py + + If you are unsure whether the virtual environment will be run by default, you can use the get-command + function: + (env_310_OpenCSP) PS C:\ctemp\select_image_points_test> get-command python + + This will show which python executable will be run. If the virtual environment is set as default, + you should see something like this: + CommandType Name Version Source + ----------- ---- ------- ------ + Application python.exe 3.10.91... C:\\Code\env_310_OpenCSP\Scripts\python.exe diff --git a/opencsp/app/select_image_points/SelectImagePoints.py b/opencsp/app/select_image_points/SelectImagePoints.py index 162da33d8..f84f6dee6 100644 --- a/opencsp/app/select_image_points/SelectImagePoints.py +++ b/opencsp/app/select_image_points/SelectImagePoints.py @@ -5,6 +5,54 @@ 'escape' key closes window. 's' key saves data. +Notes: + 1. When the window launches, it first raises a dialog to select an image file. The dialog box only + shows .RAW or .NEF files. Switch to "All files" to select images of other types (.jpg, .png, etc). + + 2. After selecting an image file, the program diplays the image. But for some reason it shows up + behind all other windows. If you minimize all other windows, then you can see the image. + Note that the display does not include the windows border, or minimize/maximize/close buttons, etc. + + 3. There are no prompts, but the program silently waits for you to click on the image somewhere. + When you do, the image will be replaced by a small tile from the image in the vicinity of where + you clicked, shown highly magnified. This enables you to select individual pixels, while seeing + surrounding context. + + 4. After you make the fine-grain selction on the enlarged window, the progrem redisplays the full + image and waits. + + 5. You can then repeat the process to select additional points. The program is logging your selections + in the background. + + 6. When you are done selecting points, press "s" to save and exit. There is no confirmation raised + or written to the console. + + 7. The program writes the selected points in a file "points_.txt", which is written to + the directory from which you launched the program. The file contains the path to the selected image. + + 8. Thus, to control the location where the list of points is written, cd to the directory you wish to + save to, and then launch the program. You can use the file selection dialog to navigate to the + image you want to select. + + For example: + (env_310_OpenCSP) PS C:\> cd C:\ctemp\select_image_points_test\ + (env_310_OpenCSP) PS C:\ctemp\select_image_points_test> python C:\\Code\OpenCSP\opencsp\app\select_image_points\SelectImagePoints.py + + The above will work if you have your default python run environment set to the virtual environment + env_310_OpenCSP. If not, then you should explicitly specify this to ensure that all of the OpenCSP + packages are avaialble. You can do this by: + PS C:\ctemp\select_image_points_test> C:\\Code\env_310_OpenCSP\Scripts\python.exe C:\\Code\OpenCSP\opencsp\app\select_image_points\SelectImagePoints.py + + If you are unsure whether the virtual environment will be run by default, you can use the get-command + function: + (env_310_OpenCSP) PS C:\ctemp\select_image_points_test> get-command python + + This will show which python executable will be run. If the virtual environment is set as default, + you should see something like this: + CommandType Name Version Source + ----------- ---- ------- ------ + Application python.exe 3.10.91... C:\\Code\env_310_OpenCSP\Scripts\python.exe + """ import tkinter as tk From 28a355bcf071ceed35115729ac6be0d00e05f6ee Mon Sep 17 00:00:00 2001 From: rcbrost Date: Tue, 23 Dec 2025 08:49:34 -0700 Subject: [PATCH 05/29] Set slope map axes equal. --- opencsp/common/lib/csp/visualize_orthorectified_image.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opencsp/common/lib/csp/visualize_orthorectified_image.py b/opencsp/common/lib/csp/visualize_orthorectified_image.py index 79b668049..e23385472 100644 --- a/opencsp/common/lib/csp/visualize_orthorectified_image.py +++ b/opencsp/common/lib/csp/visualize_orthorectified_image.py @@ -79,5 +79,6 @@ def plot_orthorectified_image( """ plt_im = axis.imshow(image, cmap, origin="lower", extent=extent) plt_im.set_clim(clims) + axis.set_aspect('equal') plt_cmap = plt.colorbar(plt_im, ax=axis) plt_cmap.ax.set_ylabel(cmap_title, rotation=270, labelpad=15) From 5175c3339efd09b4daa6af95c11a5fcdf57c4d9c Mon Sep 17 00:00:00 2001 From: rcbrost Date: Tue, 23 Dec 2025 14:52:31 -0700 Subject: [PATCH 06/29] Upgraded example_scene_reconstruction.py to allow argument specifying a configuration file. --- example/conftest.py | 15 +-- .../example_scene_reconstruction.py | 118 +++++++++--------- .../example_scene_reconstruction_README.md | 66 ++++++++++ ...le_scene_reconstruction_settings_ctemp.ini | 14 +++ opencsp/common/lib/tool/log_tools.py | 22 ++++ 5 files changed, 169 insertions(+), 66 deletions(-) create mode 100644 example/scene_reconstruction/example_scene_reconstruction_README.md create mode 100644 example/scene_reconstruction/example_scene_reconstruction_settings_ctemp.ini diff --git a/example/conftest.py b/example/conftest.py index 1e06f1ba3..0fc64dac0 100644 --- a/example/conftest.py +++ b/example/conftest.py @@ -11,12 +11,9 @@ def pytest_addoption(parser): parser.addoption('--dir_input', action='store', default='', help='Base directory with data input') parser.addoption('--dir_output', action='store', default='', help='Base directory where output will be written') - parser.addoption( - '--write_full_data', - action='store', - default='False', - help='If true, write out a directory structure including all input data. Otherwise only write generated output.', - ) + + +# parser.addoption('--verbose', action='store', default='False', help='Output detailed information.') @pytest.fixture @@ -29,6 +26,6 @@ def dir_output_fixture(request): return request.config.getoption('--dir_output') -@pytest.fixture -def write_full_data_fixture(request): - return request.config.getoption('--write_full_data') +# @pytest.fixture +# def verbose_fixture(request): +# return request.config.getoption('--verbose') diff --git a/example/scene_reconstruction/example_scene_reconstruction.py b/example/scene_reconstruction/example_scene_reconstruction.py index 719efc146..a65177f33 100644 --- a/example/scene_reconstruction/example_scene_reconstruction.py +++ b/example/scene_reconstruction/example_scene_reconstruction.py @@ -26,11 +26,11 @@ f. Using pytest as the vehicle for full-scale example execution. In OpenCSP\example directory: - pytest .\scene_reconstruction\example_scene_reconstruction.py --dir_input=C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\data_measurement --dir_output=C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\output --write_full_data=True + pytest .\scene_reconstruction\example_scene_reconstruction.py --dir_input=C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\data_measurement --dir_output=C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\output --verbose=True g. Using pytest as the vehicle for example execution on user data. In OpenCSP\example directory: - pytest .\scene_reconstruction\example_scene_reconstruction.py --dir_input= --dir_output= --write_full_data= + pytest .\scene_reconstruction\example_scene_reconstruction.py --dir_input= --dir_output= --verbose= 2. Running the example from the command line. Purpose: To apply the example calculation to new data. @@ -54,9 +54,9 @@ In OpenCSP\example\scene_reconstruction directory: python .\example_scene_reconstruction.py --settings_dir_body_ext "\\\OpenCSP_\example_scene_reconstruction_settings_.ini" ii. Output levels: - (1) Output only newly computed information. In .ini: write_full_data = False + (1) Output only newly computed information. In .ini: verbose = False (2) Output full beginning-to-end data corpus in a linear set of directories. - In .ini: write_full_data = True + In .ini: verbose = True c. Review command-line options: In OpenCSP\example\scene_reconstruction directory: @@ -78,7 +78,7 @@ """ -from os.path import join, basename, dirname +from os.path import join, basename, dirname, splitext import argparse import configparser @@ -92,7 +92,7 @@ import opencsp.common.lib.tool.log_tools as lt -def scene_reconstruction(dir_input, dir_output, write_full_data): +def scene_reconstruction(dir_input, dir_output, verbose): """ Reconstructs the XYZ locations of Aruco markers in a scene. @@ -108,8 +108,8 @@ def scene_reconstruction(dir_input, dir_output, write_full_data): - 'alignment_points.csv': CSV file with alignment points. dir_output : str The directory where the output files, including point locations and calibration figures, will be saved. - write_full_data : bool - If true, write out a directory structure including all input data. Otherwise only write generated output. + verbose : bool + If true, write out detailed information. Notes ----- @@ -165,43 +165,59 @@ def scene_reconstruction(dir_input, dir_output, write_full_data): fig.savefig(figure_path_body) -def example_scene_reconstruction_driver(dir_input_fixture, dir_output_fixture, write_full_data_fixture): +def example_scene_reconstruction_driver(arg_settings_dir_body_ext: str = None, verbose_param=None): """ Sets up and runs the scene_reconstruction() routine. Parameters ---------- - dir_input_fixture : str - Directory to read input. Has fixture suffix because it might be provided by pytest. - dir_output_fixture : str - Directory to write output. Has fixture suffix because it might be provided by pytest. - fixture_dir_write_full_data : bool - If true, write out a directory structure including all input data. Otherwise only write generated output. - Has fixture suffix because it might be provided by pytest. + arg_settings_dir_body_ext : str + Full path and filename for settings file, containing inputand output directories, plot control settings, etc. + Optional. If not provided, internal defaults are used. + See code for options sought within file. """ - dir_input = join(opencsp_code_dir(), 'app/scene_reconstruction/test/data/data_measurement') - dir_output = join(dirname(__file__), 'data/output/scene_reconstruction') - write_full_data = False - if dir_input_fixture: - dir_input = dir_input_fixture - if dir_output_fixture: - dir_output = dir_output_fixture - if write_full_data_fixture: - write_full_data = write_full_data_fixture + # Get settings + if arg_settings_dir_body_ext is None: + print("Using default control settings.") + dir_input = join(opencsp_code_dir(), 'app/scene_reconstruction/test/data/data_measurement') + dir_output = join(dirname(__file__), 'data/output/scene_reconstruction') + if verbose_param is None: + verbose = False + else: + verbose = verbose_param + else: + print("Loading control from settings file:", arg_settings_dir_body_ext) + if not ft.file_exists(arg_settings_dir_body_ext): + print("ERROR: In " + basename(__file__) + ", settings file does not exist. Settings file:") + print(" ", arg_settings_dir_body_ext) + assert False + settings = configparser.ConfigParser() + settings.read(arg_settings_dir_body_ext) + dir_input = settings["Default"]["dir_input"] + dir_output = settings["Default"]["dir_output"] + verbose_setting = settings["Default"]["verbose"] + if verbose_param is None: + verbose = verbose_setting + else: + verbose = verbose_param # Ensure output directory is ready ft.create_directories_if_necessary(dir_output) # Set up logger - logfile_dir_body_ext = join(dir_output, 'log.txt') + logfile_dir_body_ext = join(dir_output, splitext(basename(__file__))[0] + '_log.txt') + print("logfile_dir_body_ext = ", logfile_dir_body_ext) lt.logger(logfile_dir_body_ext, lt.log.INFO) + # Output standard lines. + if verbose: + lt.info_strings_from_file(join(dirname(__file__), splitext(basename(__file__))[0] + '_README.md')) lt.info('Starting program ' + __file__) - lt.info('dir_input = ' + dir_input) lt.info('dir_output = ' + dir_output) - lt.info('write_full_data = ' + str(write_full_data)) - lt.info('Calling routine scene_reconstruction(dir_input, dir_output, write_full_data)...') - scene_reconstruction(dir_input, dir_output, write_full_data) + lt.info('verbose = ' + str(verbose)) + if verbose: + lt.info('Calling routine scene_reconstruction(dir_input, dir_output, verbose)...') + scene_reconstruction(dir_input, dir_output, verbose) if __name__ == '__main__': @@ -213,7 +229,7 @@ def example_scene_reconstruction_driver(dir_input_fixture, dir_output_fixture, w # Retrieved 2025-12-04, License - CC BY-SA 4.0 parser = argparse.ArgumentParser( prog=__file__.rstrip(".py"), - description="Example scene reconstruction calculation. Given photos with Aruco markers, find marker and camera 3-d positions.", + description='Example scene reconstruction calculation. Given photos with Aruco markers, find marker and camera 3-d positions. See "example_scene_reconstruction_README.md" for details.', formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument( @@ -222,33 +238,21 @@ def example_scene_reconstruction_driver(dir_input_fixture, dir_output_fixture, w required=False, dest="settings_dir_body_ext", default=None, - help="The directory root for reading data and writing output for this run.", + help="Settings file defining run parameters (input/output directories, etc).", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + dest="verbose", + help="Output detailed information reporting run progress and calculations.", ) args = parser.parse_args() arg_settings_dir_body_ext: str = args.settings_dir_body_ext - print("arg: settings_dir_body_ext = ", arg_settings_dir_body_ext) + verbose: bool = args.verbose - # Get settings - if arg_settings_dir_body_ext is None: - print("Using default control settings.") - dir_input_main = join(opencsp_code_dir(), 'app/scene_reconstruction/test/data/data_measurement') - dir_output_main = join(dirname(__file__), 'data/output/scene_reconstruction') - write_full_data_main = False - else: - print("Loading control from settings file:", arg_settings_dir_body_ext) - if not ft.file_exists(arg_settings_dir_body_ext): - print("ERROR: In " + basename(__file__) + ", settings file does not exist. Settings file:") - print(" ", arg_settings_dir_body_ext) - assert False - settings = configparser.ConfigParser() - settings.read(arg_settings_dir_body_ext) - dir_input_main = settings["Default"]["dir_input"] - dir_output_main = settings["Default"]["dir_output"] - write_full_data_main = settings["Default"]["write_full_data"] - - # Call driver, noting status first. - print("Calling driver:") - print(" dir_main_input = ", dir_input_main) - print(" dir_main_output = ", dir_output_main) - print(" write_full_data = ", write_full_data_main) - example_scene_reconstruction_driver(dir_input_main, dir_output_main, write_full_data_main) + # Manual override for use when debugging. Comment this line for normal runs. + # verbose: bool = True + + # Call driver. + example_scene_reconstruction_driver(arg_settings_dir_body_ext, verbose) diff --git a/example/scene_reconstruction/example_scene_reconstruction_README.md b/example/scene_reconstruction/example_scene_reconstruction_README.md new file mode 100644 index 000000000..69c4b82a5 --- /dev/null +++ b/example/scene_reconstruction/example_scene_reconstruction_README.md @@ -0,0 +1,66 @@ +example_scene_reconstruction_README.txt: +======================================== +Example scene reconstruction calculation. Given photos with Aruco markers, find marker +and camera 3-d positions. See "example_scene_reconstruction_README.txt" for details. + +To run this as a pytest on the built-in input: + 1. cd to the OpenCSP code directory. + 2. cd to the example subdirectory. + 3. Execute pytest: + pytest + or + pytest scene_reconstruction\example_scene_reconstruction.py + +To run this on the default built-in input, omit the -s option: + 1. cd to the directory containing the script "example_scene_reconstruction.py". + 2. Run the script: + python example_scene_reconstruction.py --verbose + The "--verbose" flag is optional.' + +To run this on new input, use the -s option and point to a settings control file. +For an example settings file, see: + \example\scene_reconstruction\example_scene_reconstruction_settings_ctemp.ini + +We recommend copying this file and placing it alongside the data you wish to run. +For example, to run the full-size OpenCSP example:' + 1. Create a directory "C:\ctemp\OpenCSP_ctemp\example_data_large" for holding example data. + 2. Create subdirectory "scene_reconstruction" + 3. Create subsubdirectory "input", and fill it with the required input files: + "alignment_points.csv" + "camera.h5" + "known_point_locations.csv" + "point_pair_distances.csv" + Subdirectory "aruco_marker_images" containing images of scene with Aruco markers. + 4. Copy the file "example_scene_reconstruction_settings_ctemp.ini" into the subdirectory + "scene_reconstruction" made in step 2.' + 5. Launch a PowerShell and ensure the OpenCSP virtual environment is activated. + 6. cd to the directory containing the script "example_scene_reconstruction.py". + 7. Run the script, providing the -s option and pointing to the .ini file: + python example_scene_reconstruction.py --verbose -s "C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\example_scene_reconstruction_settings_ctemp.ini" + 8. The "--verbose" option generates additional status and calculation output. + 9. The output will be written to an "example_scene_reconstruction_output" subdirectory + created alongside the input directory. + (This is to distinguish it from output from other examples within the directory.) + +To run this calculation on your own data: + A. Create a directory holding your input data. + B. Copy the "example_scene_reconstruction_settings_ctemp.ini" file to a new name, such as + "My_Data_scene_reconstruction_settings.ini" and edit it to point to your data location. + C. For the sake of example, suppose you place your data in "C:\ctemp\OpenCSP_ctemp\MyData", + and also suppose you place your new "My_Data_scene_reconstruction_settings.ini" file + in this directory. + Then you can run the same calculation on your data by: + a. cd to the directory containing the script "example_scene_reconstruction.py". + b. Run the script, providing the -s option and pointing to your .ini file: + python example_scene_reconstruction.py --verbose -s "C:\ctemp\OpenCSP_ctemp\MyData\My_Data_scene_reconstruction_settings_ctemp.ini" + D. The output will be written to the output subdirectory you specify in your .ini file.' + +For a detailed description of the algorithm and its input and output, see: + B. J. Smith, R. C. Brost, and B. G. Bean. + Scene Reconstruction User Guide, Document Version 1.0. + Sandia National Laboratories Report SAND2024-10625, August 2024. + https://doi.org/10.2172/2463024 + +Also available through OpenCSP_Documents; see https://opencsp.sandia.gov + +======================================== diff --git a/example/scene_reconstruction/example_scene_reconstruction_settings_ctemp.ini b/example/scene_reconstruction/example_scene_reconstruction_settings_ctemp.ini new file mode 100644 index 000000000..5cc2f48b4 --- /dev/null +++ b/example/scene_reconstruction/example_scene_reconstruction_settings_ctemp.ini @@ -0,0 +1,14 @@ +# This file provides the data paths for the example_scene_reconstruction.py example. +# To use this file, copy it to a new filename and update the values below to point at your data source. +# Original file: +# /example/scene_reconstruction/example_scene_reconstruction_ctemp.ini + +[Default] +# Directory to find input files. +dir_input = C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\input + +# Directory to find output files. +dir_output = C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\example_scene_reconstruction_output + +# Whether to output detailed progress output, intermediate calculation results, etc. +verbose = True diff --git a/opencsp/common/lib/tool/log_tools.py b/opencsp/common/lib/tool/log_tools.py index f0f599996..f614f4a17 100644 --- a/opencsp/common/lib/tool/log_tools.py +++ b/opencsp/common/lib/tool/log_tools.py @@ -531,3 +531,25 @@ def log_progress( return log_progress(int(np.round(percentage)), carriage_return, prev_percentage) else: return log_progress(int(np.round(percentage * 100)), carriage_return, prev_percentage) + + +def info_strings_from_file(input_strings_file: str) -> None: + """ + Reads strings from input file and writes them to the log as INFO. + + Parameters + ---------- + input_strings_file : str + Text file containing strings to log. + """ + # import here instead of at the top of the file to avoid cyclic import issues + import opencsp.common.lib.tool.file_tools as ft + + if not ft.file_exists(input_strings_file): + error_and_raise( + RuntimeError, + f'Error: in log_tools.info_strings_from_file: input file does not exist: "{input_strings_file}"', + ) + with open(input_strings_file, "r") as f: + for line in f: + info(line.rstrip()) From 7221cb4926a9e9d7ce35df2c11503e6b658862f7 Mon Sep 17 00:00:00 2001 From: rcbrost Date: Fri, 5 Dec 2025 16:26:00 -0700 Subject: [PATCH 07/29] Progress in implementing actual command-line arguments and .ini file for example_scene_reconstruction.py --- .../scene_reconstruction/example_scene_reconstruction.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/example/scene_reconstruction/example_scene_reconstruction.py b/example/scene_reconstruction/example_scene_reconstruction.py index a65177f33..154ba5a6f 100644 --- a/example/scene_reconstruction/example_scene_reconstruction.py +++ b/example/scene_reconstruction/example_scene_reconstruction.py @@ -232,6 +232,12 @@ def example_scene_reconstruction_driver(arg_settings_dir_body_ext: str = None, v description='Example scene reconstruction calculation. Given photos with Aruco markers, find marker and camera 3-d positions. See "example_scene_reconstruction_README.md" for details.', formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) + # parser.add_argument( + # "--verbose", + # action="store_true", + # dest="verbose", + # help="Print more information while running. Overrides '--progress'.", + # ) parser.add_argument( "-s", "--settings_dir_body_ext", From 5811af007fe2d5b6a127778a53df56870d60b7bb Mon Sep 17 00:00:00 2001 From: rcbrost Date: Sat, 6 Dec 2025 11:45:10 -0700 Subject: [PATCH 08/29] Finished upgrading example_scene_reconstruction.py to accept arguments either for command line or pytest. --- .../example_scene_reconstruction.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/example/scene_reconstruction/example_scene_reconstruction.py b/example/scene_reconstruction/example_scene_reconstruction.py index 154ba5a6f..67a223b70 100644 --- a/example/scene_reconstruction/example_scene_reconstruction.py +++ b/example/scene_reconstruction/example_scene_reconstruction.py @@ -26,11 +26,19 @@ f. Using pytest as the vehicle for full-scale example execution. In OpenCSP\example directory: +<<<<<<< HEAD pytest .\scene_reconstruction\example_scene_reconstruction.py --dir_input=C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\data_measurement --dir_output=C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\output --verbose=True g. Using pytest as the vehicle for example execution on user data. In OpenCSP\example directory: pytest .\scene_reconstruction\example_scene_reconstruction.py --dir_input= --dir_output= --verbose= +======= + pytest .\scene_reconstruction\example_scene_reconstruction.py --dir_input=C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\data_measurement --dir_output=C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\output --write_full_data=True + + g. Using pytest as the vehicle for example execution on user data. + In OpenCSP\example directory: + pytest .\scene_reconstruction\example_scene_reconstruction.py --dir_input= --dir_output= --write_full_data= +>>>>>>> f8d52164 (Finished upgrading example_scene_reconstruction.py to accept arguments either for command line or pytest.) 2. Running the example from the command line. Purpose: To apply the example calculation to new data. @@ -54,9 +62,15 @@ In OpenCSP\example\scene_reconstruction directory: python .\example_scene_reconstruction.py --settings_dir_body_ext "\\\OpenCSP_\example_scene_reconstruction_settings_.ini" ii. Output levels: +<<<<<<< HEAD (1) Output only newly computed information. In .ini: verbose = False (2) Output full beginning-to-end data corpus in a linear set of directories. In .ini: verbose = True +======= + (1) Output only newly computed information. In .ini: write_full_data = False + (2) Output full beginning-to-end data corpus in a linear set of directories. + In .ini: write_full_data = True +>>>>>>> f8d52164 (Finished upgrading example_scene_reconstruction.py to accept arguments either for command line or pytest.) c. Review command-line options: In OpenCSP\example\scene_reconstruction directory: @@ -232,12 +246,6 @@ def example_scene_reconstruction_driver(arg_settings_dir_body_ext: str = None, v description='Example scene reconstruction calculation. Given photos with Aruco markers, find marker and camera 3-d positions. See "example_scene_reconstruction_README.md" for details.', formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) - # parser.add_argument( - # "--verbose", - # action="store_true", - # dest="verbose", - # help="Print more information while running. Overrides '--progress'.", - # ) parser.add_argument( "-s", "--settings_dir_body_ext", From e969777ae2c902aeff9589f111b797087025d13d Mon Sep 17 00:00:00 2001 From: rcbrost Date: Tue, 23 Dec 2025 14:52:31 -0700 Subject: [PATCH 09/29] Upgraded example_scene_reconstruction.py to allow argument specifying a configuration file. --- .../example_scene_reconstruction.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/example/scene_reconstruction/example_scene_reconstruction.py b/example/scene_reconstruction/example_scene_reconstruction.py index 67a223b70..380e5d380 100644 --- a/example/scene_reconstruction/example_scene_reconstruction.py +++ b/example/scene_reconstruction/example_scene_reconstruction.py @@ -26,19 +26,11 @@ f. Using pytest as the vehicle for full-scale example execution. In OpenCSP\example directory: -<<<<<<< HEAD pytest .\scene_reconstruction\example_scene_reconstruction.py --dir_input=C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\data_measurement --dir_output=C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\output --verbose=True - g. Using pytest as the vehicle for example execution on user data. - In OpenCSP\example directory: - pytest .\scene_reconstruction\example_scene_reconstruction.py --dir_input= --dir_output= --verbose= -======= - pytest .\scene_reconstruction\example_scene_reconstruction.py --dir_input=C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\data_measurement --dir_output=C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\output --write_full_data=True - g. Using pytest as the vehicle for example execution on user data. In OpenCSP\example directory: pytest .\scene_reconstruction\example_scene_reconstruction.py --dir_input= --dir_output= --write_full_data= ->>>>>>> f8d52164 (Finished upgrading example_scene_reconstruction.py to accept arguments either for command line or pytest.) 2. Running the example from the command line. Purpose: To apply the example calculation to new data. @@ -62,15 +54,9 @@ In OpenCSP\example\scene_reconstruction directory: python .\example_scene_reconstruction.py --settings_dir_body_ext "\\\OpenCSP_\example_scene_reconstruction_settings_.ini" ii. Output levels: -<<<<<<< HEAD (1) Output only newly computed information. In .ini: verbose = False (2) Output full beginning-to-end data corpus in a linear set of directories. In .ini: verbose = True -======= - (1) Output only newly computed information. In .ini: write_full_data = False - (2) Output full beginning-to-end data corpus in a linear set of directories. - In .ini: write_full_data = True ->>>>>>> f8d52164 (Finished upgrading example_scene_reconstruction.py to accept arguments either for command line or pytest.) c. Review command-line options: In OpenCSP\example\scene_reconstruction directory: @@ -92,6 +78,7 @@ """ +from os.path import join, basename, dirname, splitext from os.path import join, basename, dirname, splitext import argparse @@ -106,6 +93,7 @@ import opencsp.common.lib.tool.log_tools as lt +def scene_reconstruction(dir_input, dir_output, verbose): def scene_reconstruction(dir_input, dir_output, verbose): """ Reconstructs the XYZ locations of Aruco markers in a scene. From b415cf01b146b27dee96bf67cd1b20de6936f769 Mon Sep 17 00:00:00 2001 From: rcbrost Date: Fri, 5 Dec 2025 10:55:20 -0700 Subject: [PATCH 10/29] Exploring command-line arguments and experiment_settings.ini files. Rough rough example_scene_reconstruction.py working from the command line. --- example/scene_reconstruction/example_scene_reconstruction.py | 1 - 1 file changed, 1 deletion(-) diff --git a/example/scene_reconstruction/example_scene_reconstruction.py b/example/scene_reconstruction/example_scene_reconstruction.py index 380e5d380..0d398aed8 100644 --- a/example/scene_reconstruction/example_scene_reconstruction.py +++ b/example/scene_reconstruction/example_scene_reconstruction.py @@ -93,7 +93,6 @@ import opencsp.common.lib.tool.log_tools as lt -def scene_reconstruction(dir_input, dir_output, verbose): def scene_reconstruction(dir_input, dir_output, verbose): """ Reconstructs the XYZ locations of Aruco markers in a scene. From 55ffc345f0ee16bdb3035dbb274160062a9c526d Mon Sep 17 00:00:00 2001 From: rcbrost Date: Fri, 5 Dec 2025 16:26:00 -0700 Subject: [PATCH 11/29] Progress in implementing actual command-line arguments and .ini file for example_scene_reconstruction.py --- .../scene_reconstruction/example_scene_reconstruction.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/example/scene_reconstruction/example_scene_reconstruction.py b/example/scene_reconstruction/example_scene_reconstruction.py index 0d398aed8..36a4b9b4f 100644 --- a/example/scene_reconstruction/example_scene_reconstruction.py +++ b/example/scene_reconstruction/example_scene_reconstruction.py @@ -233,6 +233,12 @@ def example_scene_reconstruction_driver(arg_settings_dir_body_ext: str = None, v description='Example scene reconstruction calculation. Given photos with Aruco markers, find marker and camera 3-d positions. See "example_scene_reconstruction_README.md" for details.', formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) + # parser.add_argument( + # "--verbose", + # action="store_true", + # dest="verbose", + # help="Print more information while running. Overrides '--progress'.", + # ) parser.add_argument( "-s", "--settings_dir_body_ext", From 4dfaf18ba2bbdf96360b6fb2a4a6fd11d92a57f2 Mon Sep 17 00:00:00 2001 From: rcbrost Date: Tue, 23 Dec 2025 17:07:09 -0700 Subject: [PATCH 12/29] Resolved conflicts with example_scene_reconstruction.py --- .../scene_reconstruction/example_scene_reconstruction.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/example/scene_reconstruction/example_scene_reconstruction.py b/example/scene_reconstruction/example_scene_reconstruction.py index 36a4b9b4f..8fa69770c 100644 --- a/example/scene_reconstruction/example_scene_reconstruction.py +++ b/example/scene_reconstruction/example_scene_reconstruction.py @@ -78,7 +78,6 @@ """ -from os.path import join, basename, dirname, splitext from os.path import join, basename, dirname, splitext import argparse @@ -233,12 +232,6 @@ def example_scene_reconstruction_driver(arg_settings_dir_body_ext: str = None, v description='Example scene reconstruction calculation. Given photos with Aruco markers, find marker and camera 3-d positions. See "example_scene_reconstruction_README.md" for details.', formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) - # parser.add_argument( - # "--verbose", - # action="store_true", - # dest="verbose", - # help="Print more information while running. Overrides '--progress'.", - # ) parser.add_argument( "-s", "--settings_dir_body_ext", From 8db84879f90f6bb6daeb3e77ffe3cf1b212c810e Mon Sep 17 00:00:00 2001 From: rcbrost Date: Tue, 23 Dec 2025 14:52:31 -0700 Subject: [PATCH 13/29] Upgraded example_scene_reconstruction.py to allow argument specifying a configuration file. --- example/scene_reconstruction/example_scene_reconstruction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/scene_reconstruction/example_scene_reconstruction.py b/example/scene_reconstruction/example_scene_reconstruction.py index 8fa69770c..a65177f33 100644 --- a/example/scene_reconstruction/example_scene_reconstruction.py +++ b/example/scene_reconstruction/example_scene_reconstruction.py @@ -30,7 +30,7 @@ g. Using pytest as the vehicle for example execution on user data. In OpenCSP\example directory: - pytest .\scene_reconstruction\example_scene_reconstruction.py --dir_input= --dir_output= --write_full_data= + pytest .\scene_reconstruction\example_scene_reconstruction.py --dir_input= --dir_output= --verbose= 2. Running the example from the command line. Purpose: To apply the example calculation to new data. From f7717bf959a9dd4246f18b5ebd6fcaa8b13250cc Mon Sep 17 00:00:00 2001 From: rcbrost Date: Fri, 23 Jan 2026 10:35:05 -0700 Subject: [PATCH 14/29] Resolved conflicts in example_scene_reconstruction.py. --- .../scene_reconstruction/example_scene_reconstruction.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/example/scene_reconstruction/example_scene_reconstruction.py b/example/scene_reconstruction/example_scene_reconstruction.py index a65177f33..154ba5a6f 100644 --- a/example/scene_reconstruction/example_scene_reconstruction.py +++ b/example/scene_reconstruction/example_scene_reconstruction.py @@ -232,6 +232,12 @@ def example_scene_reconstruction_driver(arg_settings_dir_body_ext: str = None, v description='Example scene reconstruction calculation. Given photos with Aruco markers, find marker and camera 3-d positions. See "example_scene_reconstruction_README.md" for details.', formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) + # parser.add_argument( + # "--verbose", + # action="store_true", + # dest="verbose", + # help="Print more information while running. Overrides '--progress'.", + # ) parser.add_argument( "-s", "--settings_dir_body_ext", From bcb189e0d8bbfeb06cb12f6eb59db85ee1f2546b Mon Sep 17 00:00:00 2001 From: rcbrost Date: Fri, 23 Jan 2026 10:41:52 -0700 Subject: [PATCH 15/29] Resolved merge conflicts. --- .../scene_reconstruction/example_scene_reconstruction.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/example/scene_reconstruction/example_scene_reconstruction.py b/example/scene_reconstruction/example_scene_reconstruction.py index 154ba5a6f..a65177f33 100644 --- a/example/scene_reconstruction/example_scene_reconstruction.py +++ b/example/scene_reconstruction/example_scene_reconstruction.py @@ -232,12 +232,6 @@ def example_scene_reconstruction_driver(arg_settings_dir_body_ext: str = None, v description='Example scene reconstruction calculation. Given photos with Aruco markers, find marker and camera 3-d positions. See "example_scene_reconstruction_README.md" for details.', formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) - # parser.add_argument( - # "--verbose", - # action="store_true", - # dest="verbose", - # help="Print more information while running. Overrides '--progress'.", - # ) parser.add_argument( "-s", "--settings_dir_body_ext", From 378070ae2647987bcd11e72fd197d0a3039391e0 Mon Sep 17 00:00:00 2001 From: rcbrost Date: Wed, 24 Dec 2025 05:35:56 -0700 Subject: [PATCH 16/29] Updated main docstring in example_scene_reconstruction.py. --- .../example_scene_reconstruction.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/example/scene_reconstruction/example_scene_reconstruction.py b/example/scene_reconstruction/example_scene_reconstruction.py index a65177f33..3136d1fed 100644 --- a/example/scene_reconstruction/example_scene_reconstruction.py +++ b/example/scene_reconstruction/example_scene_reconstruction.py @@ -24,14 +24,6 @@ In OpenCSP\example directory: pytest .\scene_reconstruction\example_scene_reconstruction.py - f. Using pytest as the vehicle for full-scale example execution. - In OpenCSP\example directory: - pytest .\scene_reconstruction\example_scene_reconstruction.py --dir_input=C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\data_measurement --dir_output=C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\output --verbose=True - - g. Using pytest as the vehicle for example execution on user data. - In OpenCSP\example directory: - pytest .\scene_reconstruction\example_scene_reconstruction.py --dir_input= --dir_output= --verbose= - 2. Running the example from the command line. Purpose: To apply the example calculation to new data. @@ -54,9 +46,8 @@ In OpenCSP\example\scene_reconstruction directory: python .\example_scene_reconstruction.py --settings_dir_body_ext "\\\OpenCSP_\example_scene_reconstruction_settings_.ini" ii. Output levels: - (1) Output only newly computed information. In .ini: verbose = False - (2) Output full beginning-to-end data corpus in a linear set of directories. - In .ini: verbose = True + (1) Output only minimal progress updates. Omit --verbose flag. + (2) Output full progress and calculation updates. Add --verbose flag. c. Review command-line options: In OpenCSP\example\scene_reconstruction directory: @@ -66,7 +57,7 @@ Purpose: To utilize the example calculation within a larger computation or application. a. Arbitrary context and general data. - In calling file, import example_scenario_reconstruction, call driver. (??other name??) + In calling file, import example_scenario_reconstruction, call driver. 4. Running an example with the Visual Studio Code debugger. Purpose: To interact with the code execution (break points, check stack variables, etc), either for study or fixing an error. Could apply to either 2 or 3 above. @@ -74,7 +65,7 @@ a. Use default settings, running on default data. In VS Code, press F5 key. b. Temporarily modify internal variable values to test specific data computation. - Edit in VS Code, then press F5 key. + Make a scratch copy somewhere else, edit in VS Code, then press F5 key. """ From e3231363611e38b02a5185992994a3fdd951f937 Mon Sep 17 00:00:00 2001 From: rcbrost Date: Wed, 24 Dec 2025 15:18:19 -0700 Subject: [PATCH 17/29] Restored SofastCommandLineInterface.py --- .../app/sofast/SofastCommandLineInterface.py | 583 ++++++++++++++++++ 1 file changed, 583 insertions(+) create mode 100644 contrib/app/sofast/SofastCommandLineInterface.py diff --git a/contrib/app/sofast/SofastCommandLineInterface.py b/contrib/app/sofast/SofastCommandLineInterface.py new file mode 100644 index 000000000..a3300eea1 --- /dev/null +++ b/contrib/app/sofast/SofastCommandLineInterface.py @@ -0,0 +1,583 @@ +"""A script that runs SOFAST in a command-line manner. We recommend copying this file into +your working directory and modifying it there. To run SOFAST, perform the following steps: + +1. Navigate to the bottom of this file and fill in all user-input data specific to your SOFAST run. +2. Type "help" to display help message +3. Run SOFAST. Collect only measurement data, or (optionally) process data. + +NOTE: To update any of the parameters in the bottom of the file, the CLI must be +restarted for changes to take effect. + +TODO: +Refactor so that common code is separate from end-of-file input/execution block, move this to common. + +Make this a "kitchen sink" file, which includes all aspects: +1. Data collection: + - Fringe measurement + - Fixed measurement with projector + - Fixed measurement with printed target in ambient lght +2. Data analysis -- finding the best-fit instance of the class of shapes. + +3. Fitting to a desired reference optical shape. (Make this an enhancement issue added to SOFAST. Then, another file?) + +4. Plotting/ray tracing + +(Suggest puttting calibration in another file.) + +This file contains 1 and 2. + +In output logs, include user input. +""" + +import glob +from os.path import join, dirname, abspath + +import matplotlib.pyplot as plt + +from opencsp.app.sofast.lib.DefinitionFacet import DefinitionFacet +from opencsp.app.sofast.lib.DisplayShape import DisplayShape +from opencsp.app.sofast.lib.DotLocationsFixedPattern import DotLocationsFixedPattern +from opencsp.app.sofast.lib.Fringes import Fringes +from opencsp.app.sofast.lib.ImageCalibrationScaling import ImageCalibrationScaling +from opencsp.app.sofast.lib.ProcessSofastFixed import ProcessSofastFixed +from opencsp.app.sofast.lib.ProcessSofastFringe import ProcessSofastFringe +from opencsp.app.sofast.lib.SpatialOrientation import SpatialOrientation +from opencsp.app.sofast.lib.SystemSofastFringe import SystemSofastFringe +from opencsp.app.sofast.lib.SystemSofastFixed import SystemSofastFixed +from opencsp.common.lib.camera.Camera import Camera +from opencsp.common.lib.camera.ImageAcquisition_DCAM_mono import ImageAcquisition +from opencsp.common.lib.camera.image_processing import highlight_saturation +from opencsp.common.lib.camera.LiveView import LiveView +from opencsp.common.lib.deflectometry.ImageProjection import ImageProjection +from opencsp.common.lib.deflectometry.Surface2DParabolic import Surface2DParabolic +from opencsp.common.lib.geometry.Vxy import Vxy +from opencsp.common.lib.geometry.Vxyz import Vxyz +import opencsp.common.lib.render.figure_management as fm +import opencsp.common.lib.render_control.RenderControlAxis as rca +import opencsp.common.lib.render_control.RenderControlFigure as rcfg +import opencsp.common.lib.tool.log_tools as lt +from opencsp.common.lib.tool.time_date_tools import current_date_time_string_forfile as timestamp + + +class SofastCommandLineInterface: + """Sofast Command Line Interface class.""" + + def __init__(self) -> "SofastCommandLineInterface": + # Common sofast parameters + self.image_acquisition: ImageAcquisition = None + self.camera: Camera = None + self.facet_definition: DefinitionFacet = None + self.spatial_orientation: SpatialOrientation = None + self.measure_point_optic: Vxyz = None + self.dist_optic_screen: float = None + self.name_optic: str = None + + self.image_projection = ImageProjection.instance() + self.res_plot = 0.002 # meters + """Resolution of slope map image (meters)""" + self.colorbar_limit = 7 # mrad + """Colorbar limits in slope magnitude plot (mrad)""" + + # Sofast fringe specific + self.system_fringe: SystemSofastFringe = None + self.display_shape: DisplayShape = None + self.surface_fringe: Surface2DParabolic = None + self.timestamp_fringe_measurement = None + + # Sofast fixed specific + self.system_fixed: SystemSofastFixed = None + self.process_sofast_fixed: ProcessSofastFixed = None + self.fixed_pattern_dot_locs: DotLocationsFixedPattern = None + self.surface_fixed: Surface2DParabolic = None + self.origin: Vxy = None + self.timestamp_fixed_measurement = None + self.pattern_width = 3 + """Fixed pattern dot width, pixels""" + self.pattern_spacing = 6 + """Fixed pattern dot spacing, pixels""" + + # Save directories + self.dir_save_fringe: str = "" + """Location to save Sofast Fringe measurement data""" + self.dir_save_fixed: str = "" + """Location to save Sofast Fixed measurement data""" + self.dir_save_fringe_calibration: str = "" + """Location to save Sofast Fringe calibration data""" + + def run(self) -> None: + """Runs command line Sofast""" + self.func_user_input() + + def set_common_data( + self, + image_acquisition: ImageAcquisition, + image_projection: ImageProjection, + camera: Camera, + facet_definition: DefinitionFacet, + spatial_orientation: SpatialOrientation, + measure_point_optic: Vxyz, + dist_optic_screen: float, + name_optic: str, + ) -> None: + """Sets common parametes for Sofast Fringe and Fixed + + Parameters + ---------- + image_acquisition : ImageAcquisition + ImageAcquisition object + image_projection : ImageProjection + Image projection object + camera : Camera + Camera calibration object + facet_definition : DefinitionFacet + Facet definition object + spatial_orientation : SpatialOrientation + Calibrated SpatialOrientation object + measure_point_optic : Vxyz + Measure point location on optic + dist_optic_screen : float + Distance from measure point to screen center + name_optic : str + Name of optic + """ + self.image_acquisition = image_acquisition + self.image_projection = image_projection + self.camera = camera + self.facet_definition = facet_definition + self.spatial_orientation = spatial_orientation + self.measure_point_optic = measure_point_optic + self.dist_optic_screen = dist_optic_screen + self.name_optic = name_optic + + def set_sofast_fringe_data( + self, display_shape: DisplayShape, fringes: Fringes, surface_fringe: Surface2DParabolic + ) -> None: + """Loads Sofast Fringe specific objects + + Parameters + ---------- + display_shape : DisplayShape + Calibrated DisplayShape object + fringes : Fringes + Fringe objects to display + surface_fringe : Surface2DParabolic + Surface to use when processing Sofast Fringe data + """ + self.system_fringe = SystemSofastFringe(self.image_acquisition) + self.system_fringe.set_fringes(fringes) + self.display_shape = display_shape + self.surface_fringe = surface_fringe + + def set_sofast_fixed_data( + self, fixed_pattern_dot_locs: DotLocationsFixedPattern, origin: Vxy, surface_fixed: Surface2DParabolic + ) -> None: + """Loads Sofast Fixed specific objects + + Parameters + ---------- + fixed_pattern_dot_locs : DotLocationsFixedPattern + Calibrated dot locations object + origin : Vxy + Origin dot location in image, pixels + surface_fixed : Surface2DParabolic + Surface to use when processing Sofast Fringe data + """ + self.system_fixed = SystemSofastFixed(self.image_acquisition) + self.system_fixed.set_pattern_parameters(self.pattern_width, self.pattern_spacing) + self.fixed_pattern_dot_locs = fixed_pattern_dot_locs + self.origin = origin + self.surface_fixed = surface_fixed + + self.process_sofast_fixed = ProcessSofastFixed(self.spatial_orientation, self.camera, fixed_pattern_dot_locs) + + def func_run_fringe_measurement(self) -> None: + """Runs sofast fringe measurement""" + lt.info(f"{timestamp():s} Starting Sofast Fringe measurement") + self.timestamp_fringe_measurement = timestamp() + + def _on_done(): + lt.info(f"{timestamp():s} Completed Sofast Fringe measurement") + self.system_fringe.run_next_in_queue() + + self.system_fringe.run_measurement(_on_done) + + def func_run_fixed_measurement(self) -> None: + """Runs Sofast Fixed measurement""" + lt.info(f"{timestamp():s} Starting Sofast Fixed measurement") + self.timestamp_fixed_measurement = timestamp() + + def _f1(): + lt.info(f"{timestamp():s} Completed Sofast Fixed measurement") + self.system_fixed.run_next_in_queue() + + self.system_fixed.prepend_to_queue([self.system_fixed.run_measurement, _f1]) + self.system_fixed.run_next_in_queue() + + def func_show_crosshairs_fringe(self): + """Shows crosshairs and run next in Sofast fringe queue after a 0.2s wait""" + self.image_projection.show_crosshairs() + self.system_fringe.root.after(200, self.system_fringe.run_next_in_queue) + + def func_process_sofast_fringe_data(self): + """Processes Sofast Fringe data""" + lt.info(f"{timestamp():s} Starting Sofast Fringe data processing") + + # Get Measurement object + measurement = self.system_fringe.get_measurements( + self.measure_point_optic, self.dist_optic_screen, self.name_optic + )[0] + + # Calibrate fringe images + measurement.calibrate_fringe_images(self.system_fringe.calibration) + + # Instantiate ProcessSofastFringe + sofast = ProcessSofastFringe(measurement, self.spatial_orientation, self.camera, self.display_shape) + + # Process + sofast.process_optic_singlefacet(self.facet_definition, self.surface_fringe) + + lt.info(f"{timestamp():s} Completed Sofast Fringe data processing") + + # Plot optic + mirror = sofast.get_optic().mirror + + lt.debug(f"{timestamp():s} Plotting Sofast Fringe data") + figure_control = rcfg.RenderControlFigure(tile_array=(1, 1), tile_square=True) + axis_control_m = rca.meters() + fig_record = fm.setup_figure(figure_control, axis_control_m, title="") + mirror.plot_orthorectified_slope(self.res_plot, clim=self.colorbar_limit, axis=fig_record.axis) + fig_record.save(self.dir_save_fringe, f"{self.timestamp_fringe_measurement:s}_slope_magnitude_fringe", "png") + fig_record.close() + + # Save processed sofast data + sofast.save_to_hdf(f"{self.dir_save_fringe:s}/{self.timestamp_fringe_measurement:s}_data_sofast_fringe.h5") + lt.debug(f"{timestamp():s} Sofast Fringe data saved to HDF5") + + # Continue + self.system_fringe.run_next_in_queue() + + def func_process_sofast_fixed_data(self): + """Process Sofast Fixed data""" + lt.info(f"{timestamp():s} Starting Sofast Fixed data processing") + # Get Measurement object + measurement = self.system_fixed.get_measurement( + self.measure_point_optic, self.dist_optic_screen, self.origin, name=self.name_optic + ) + self.process_sofast_fixed.load_measurement_data(measurement) + + # Process + xy_known = (0, 0) + self.process_sofast_fixed.process_single_facet_optic( + self.facet_definition, self.surface_fixed, self.origin, xy_known=xy_known + ) + + lt.info(f"{timestamp():s} Completed Sofast Fixed data processing") + + # Plot optic + mirror = self.process_sofast_fixed.get_optic() + + lt.debug(f"{timestamp():s} Plotting Sofast Fixed data") + figure_control = rcfg.RenderControlFigure(tile_array=(1, 1), tile_square=True) + axis_control_m = rca.meters() + fig_record = fm.setup_figure(figure_control, axis_control_m, title="") + mirror.plot_orthorectified_slope(self.res_plot, clim=self.colorbar_limit, axis=fig_record.axis) + fig_record.save(self.dir_save_fixed, f"{self.timestamp_fixed_measurement:s}_slope_magnitude_fixed", "png") + + # Save processed sofast data + self.process_sofast_fixed.save_to_hdf( + f"{self.dir_save_fixed:s}/{self.timestamp_fixed_measurement:s}_data_sofast_fixed.h5" + ) + lt.debug(f"{timestamp():s} Sofast Fixed data saved to HDF5") + + # Continue + self.system_fixed.run_next_in_queue() + + def func_save_measurement_fringe(self): + """Saves measurement to HDF file""" + measurement = self.system_fringe.get_measurements( + self.measure_point_optic, self.dist_optic_screen, self.name_optic + )[0] + file = f"{self.dir_save_fringe:s}/{self.timestamp_fringe_measurement:s}_measurement_fringe.h5" + measurement.save_to_hdf(file) + self.system_fringe.calibration.save_to_hdf(file) + self.system_fringe.run_next_in_queue() + + def func_save_measurement_fixed(self): + """Save fixed measurement files""" + measurement = self.system_fixed.get_measurement( + self.measure_point_optic, self.dist_optic_screen, self.origin, name=self.name_optic + ) + measurement.save_to_hdf(f"{self.dir_save_fixed:s}/{self.timestamp_fixed_measurement:s}_measurement_fixed.h5") + self.system_fixed.run_next_in_queue() + + def func_load_last_sofast_fringe_image_cal(self): + """Loads last ImageCalibration object""" + # Find file + files = glob.glob(join(self.dir_save_fringe_calibration, "image_calibration_scaling*.h5")) + files.sort() + + if len(files) == 0: + lt.error(f"No previous calibration files found in {self.dir_save_fringe_calibration}") + return + + # Get latest file and set + file = files[-1] + image_calibration = ImageCalibrationScaling.load_from_hdf(file) + self.system_fringe.set_calibration(image_calibration) + lt.info(f"{timestamp()} Loaded image calibration file: {file}") + + def func_gray_levels_cal(self): + """Runs gray level calibration sequence""" + file = join(self.dir_save_fringe_calibration, f"image_calibration_scaling_{timestamp():s}.h5") + self.system_fringe.run_gray_levels_cal( + ImageCalibrationScaling, + file, + on_processed=self.func_user_input, + on_processing=self.func_show_crosshairs_fringe, + ) + + def show_cam_image(self): + """Shows a camera image""" + image = self.image_acquisition.get_frame() + image_proc = highlight_saturation(image, self.image_acquisition.max_value) + plt.imshow(image_proc) + plt.show() + + def show_live_view(self): + """Shows live vieew window""" + LiveView(self.image_acquisition) + + def func_user_input(self): + """Waits for user input""" + retval = input("Input: ") + + lt.debug(f"{timestamp():s} user input: {retval:s}") + + try: + self._run_given_input(retval) + except Exception as error: + lt.error(repr(error)) + self.func_user_input() + + def _check_fringe_system_loaded(self) -> bool: + if self.system_fringe is None: + lt.error(f"{timestamp()} No Sofast Fringe system loaded") + return False + return True + + def _check_fixed_system_loaded(self) -> bool: + if self.system_fixed is None: + lt.error(f"{timestamp()} No Sofast Fixed system loaded") + return False + return True + + def _run_given_input(self, retval: str) -> None: + """Runs the given command""" + # Run fringe measurement and process/save + if retval == "help": + print("\n") + print("Value Command") + print("------------------") + print("mrp run Sofast Fringe measurement and process/save") + print("mrs run Sofast Fringe measurement and save only") + print("mip run Sofast Fixed measurement and process/save") + print("mis run Sofast Fixed measurement and save only") + print("ce calibrate camera exposure") + print("cr calibrate camera-projector response") + print("lr load most recent camera-projector response calibration file") + print("q quit and close all") + print("im show image from camera.") + print("lv shows camera live view") + print("cross show crosshairs") + self.func_user_input() + elif retval == "mrp": + lt.info(f"{timestamp()} Running Sofast Fringe measurement and processing/saving data") + if self._check_fringe_system_loaded(): + funcs = [ + self.func_run_fringe_measurement, + self.func_show_crosshairs_fringe, + self.func_process_sofast_fringe_data, + self.func_save_measurement_fringe, + self.func_user_input, + ] + self.system_fringe.set_queue(funcs) + self.system_fringe.run() + else: + self.func_user_input() + # Run fringe measurement and save + elif retval == "mrs": + lt.info(f"{timestamp()} Running Sofast Fringe measurement and saving data") + if self._check_fringe_system_loaded(): + funcs = [ + self.func_run_fringe_measurement, + self.func_show_crosshairs_fringe, + self.func_save_measurement_fringe, + self.func_user_input, + ] + self.system_fringe.set_queue(funcs) + self.system_fringe.run() + else: + self.func_user_input() + # Run fixed measurement and process/save + elif retval == "mip": + lt.info(f"{timestamp()} Running Sofast Fixed measurement and processing/saving data") + if self._check_fixed_system_loaded(): + funcs = [ + self.func_run_fixed_measurement, + self.func_process_sofast_fixed_data, + self.func_save_measurement_fixed, + self.func_user_input, + ] + self.system_fixed.set_queue(funcs) + self.system_fixed.run() + else: + self.func_user_input() + # Run fixed measurement and save + elif retval == "mis": + lt.info(f"{timestamp()} Running Sofast Fixed measurement and saving data") + if self._check_fixed_system_loaded(): + funcs = [self.func_run_fixed_measurement, self.func_save_measurement_fixed, self.func_user_input] + self.system_fixed.set_queue(funcs) + self.system_fixed.run() + else: + self.func_user_input() + # Calibrate exposure time + elif retval == "ce": + lt.info(f"{timestamp()} Calibrating camera exposure") + self.image_acquisition.calibrate_exposure() + self.func_user_input() + # Calibrate response + elif retval == "cr": + lt.info(f"{timestamp()} Calibrating camera-projector response") + if self._check_fringe_system_loaded(): + funcs = [self.func_gray_levels_cal] + self.system_fringe.set_queue(funcs) + self.system_fringe.run() + else: + self.func_user_input() + # Load last fringe calibration file + elif retval == "lr": + lt.info(f"{timestamp()} Loading response calibration") + if self._check_fringe_system_loaded(): + self.func_load_last_sofast_fringe_image_cal() + self.func_user_input() + # Quit + elif retval == "q": + lt.info(f"{timestamp():s} quitting") + if self.system_fringe is not None: + self.system_fringe.close_all() + if self.system_fixed is not None: + self.system_fixed.close_all() + return + # Show single camera image + elif retval == "im": + self.show_cam_image() + self.func_user_input() + # Show camera live view + elif retval == "lv": + self.show_live_view() + self.func_user_input() + # Project crosshairs + elif retval == "cross": + self.image_projection.show_crosshairs() + self.func_user_input() + else: + lt.error(f"{timestamp()} Command, {retval}, not recognized") + self.func_user_input() + + +# Start program +if __name__ == "__main__": + # Define main directory in which to save captured/processed data + # ============================================================== + + # Define upper level save direcory + dir_save = abspath(join(dirname(__file__), "../../../../sofast_cli")) + + # Define logger directory and set up logger + dir_log = join(dir_save, "logs") + lt.logger(join(dir_log, f"log_{timestamp():s}.txt"), lt.log.INFO) + + # Define the file locations of all SOFAST calibration data + # ======================================================== + + # Define directory containing Sofast calibration files + dir_cal = abspath(join(dirname(__file__), "../../../../sofast_calibration_files")) + + # Define data files + file_facet_definition_json = join(dir_cal, "facet_NSTTF.json") + file_spatial_orientation = join(dir_cal, "spatial_orientation_optics_lab_landscape.h5") + file_display = join(dir_cal, "display_shape_optics_lab_landscape_square_distorted_3d_100x100.h5") + file_camera = join(dir_cal, "camera_sofast_optics_lab_landscape.h5") + file_image_projection = join(dir_cal, "image_projection_optics_lab_landscape_square.h5") + file_dot_locs = join(dir_cal, "dot_locations_optics_lab_landscape_square_width3_space6.h5") + + # Instantiate Sofast Command Line Interface + # ========================================= + sofast_cli = SofastCommandLineInterface() + + # Define specific sofast save directories + # ======================================= + sofast_cli.dir_save_fringe = join(dir_save, "sofast_fringe") + sofast_cli.dir_save_fringe_calibration = join(sofast_cli.dir_save_fringe, "calibration") + sofast_cli.dir_save_fixed = join(dir_save, "sofast_fixed") + + # Define camera (ImageAcquisition) parameters + # =========================================== + image_acquisition_in = ImageAcquisition(instance=0) # First camera instance found + image_acquisition_in.frame_size = (1626, 1236) # Set frame size + image_acquisition_in.gain = 230 # Set gain (higher=faster/more noise, lower=slower/less noise) + + # Define projector/display (ImageProjection) parameters + # ===================================================== + image_projection_in = ImageProjection.load_from_hdf(file_image_projection) + image_projection_in.display_data.image_delay_ms = 200 # define projector-camera delay + + # Define measurement-specific inputs + # ================================== + + # NOTE: to update any of these fields, change them here and restart the SOFAST CLI + measure_point_optic_in = Vxyz((0, 0, 0)) # Measure point on optic, meters + dist_optic_screen_in = 10.158 # Measured optic-screen distance, meters + name_optic_in = "Test optic" # Optic name + + # Load all other calibration files + # ================================ + camera_in = Camera.load_from_hdf(file_camera) # SOFAST camera definition + facet_definition_in = DefinitionFacet.load_from_json(file_facet_definition_json) # Facet definition + spatial_orientation_in = SpatialOrientation.load_from_hdf(file_spatial_orientation) # Spatial orientation + + sofast_cli.set_common_data( + image_acquisition_in, + image_projection_in, + camera_in, + facet_definition_in, + spatial_orientation_in, + measure_point_optic_in, + dist_optic_screen_in, + name_optic_in, + ) + + sofast_cli.colorbar_limit = 2 # Update plotting colorbar limit, mrad + + # Set Sofast Fringe parameters + # ============================ + display_shape_in = DisplayShape.load_from_hdf(file_display) + fringes_in = Fringes.from_num_periods(4, 4) + surface_fringe_in = Surface2DParabolic((100.0, 100.0), False, 10) + + sofast_cli.set_sofast_fringe_data(display_shape_in, fringes_in, surface_fringe_in) + + # Set Sofast Fixed parameters + # =========================== + # NOTE: to get the value of "origin_in," the user must first start the CLI, bring up the + # camera live view, then manually find the location of the (0, 0) fixed pattern dot. + fixed_pattern_dot_locs_in = DotLocationsFixedPattern.load_from_hdf(file_dot_locs) + origin_in = Vxy((1100, 560)) # pixels, location of (0, 0) dot in camera image + surface_fixed_in = Surface2DParabolic((100.0, 100.0), False, 1) + + sofast_cli.set_sofast_fixed_data(fixed_pattern_dot_locs_in, origin_in, surface_fixed_in) + + # Run + # === + sofast_cli.run() From 53751224b30b10c5f8dffa407b83ce6857e5a147 Mon Sep 17 00:00:00 2001 From: rcbrost Date: Wed, 24 Dec 2025 17:22:50 -0700 Subject: [PATCH 18/29] Progress toward upgrading example_process_single_facet.py --- .../example_scene_reconstruction.py | 4 + ...le_scene_reconstruction_settings_ctemp.ini | 8 +- .../example_process_single_facet.py | 166 ++++++++++++++---- .../example_process_single_facet_README.md | 66 +++++++ 4 files changed, 210 insertions(+), 34 deletions(-) create mode 100644 example/sofast_fringe/example_process_single_facet_README.md diff --git a/example/scene_reconstruction/example_scene_reconstruction.py b/example/scene_reconstruction/example_scene_reconstruction.py index 3136d1fed..8de4bac3a 100644 --- a/example/scene_reconstruction/example_scene_reconstruction.py +++ b/example/scene_reconstruction/example_scene_reconstruction.py @@ -162,10 +162,14 @@ def example_scene_reconstruction_driver(arg_settings_dir_body_ext: str = None, v Parameters ---------- + arg_settings_dir_body_ext : str Full path and filename for settings file, containing inputand output directories, plot control settings, etc. Optional. If not provided, internal defaults are used. See code for options sought within file. + + verbose : bool + If true, output detailed progress and calculation output. """ # Get settings if arg_settings_dir_body_ext is None: diff --git a/example/scene_reconstruction/example_scene_reconstruction_settings_ctemp.ini b/example/scene_reconstruction/example_scene_reconstruction_settings_ctemp.ini index 5cc2f48b4..7d250490e 100644 --- a/example/scene_reconstruction/example_scene_reconstruction_settings_ctemp.ini +++ b/example/scene_reconstruction/example_scene_reconstruction_settings_ctemp.ini @@ -4,11 +4,11 @@ # /example/scene_reconstruction/example_scene_reconstruction_ctemp.ini [Default] +# Whether to output detailed progress output, intermediate calculation results, etc. +verbose = True + # Directory to find input files. dir_input = C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\input -# Directory to find output files. +# Directory to write output files. dir_output = C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\example_scene_reconstruction_output - -# Whether to output detailed progress output, intermediate calculation results, etc. -verbose = True diff --git a/example/sofast_fringe/example_process_single_facet.py b/example/sofast_fringe/example_process_single_facet.py index af1c3cdb5..04154dc6d 100644 --- a/example/sofast_fringe/example_process_single_facet.py +++ b/example/sofast_fringe/example_process_single_facet.py @@ -27,7 +27,10 @@ """ import json -from os.path import join, dirname +from os.path import join, basename, dirname, splitext + +import argparse +import configparser import imageio.v3 as imageio @@ -51,7 +54,16 @@ import opencsp.common.lib.tool.log_tools as lt -def example_process_single_facet(): +def example_process_single_facet( + verbose: bool, + file_camera: str, + file_display: str, + file_orientation: str, + file_facet: str, + file_calibration: str, + file_measurement: str, + dir_save: str, +): """Performs processing of previously collected SOFAST data of single facet mirror. 1. Load saved single facet SOFAST collection data from HDF5 file @@ -63,38 +75,12 @@ def example_process_single_facet(): # General setup # ============= - # Define save dir - dir_save = join(dirname(__file__), "data/output/single_facet") + # Set up save dir ft.create_directories_if_necessary(dir_save) # Set up logger lt.logger(join(dir_save, "log.txt"), lt.log.WARN) - # Define sample data directory - dir_data_sofast = join(opencsp_code_dir(), "test/data/sofast_fringe") - dir_data_common = join(opencsp_code_dir(), "test/data/sofast_common") - - # Directory Setup - file_measurement = join(dir_data_sofast, "data_measurement/measurement_facet.h5") - file_camera = join(dir_data_common, "camera_sofast_downsampled.h5") - file_display = join(dir_data_common, "display_distorted_2d.h5") - file_orientation = join(dir_data_common, "spatial_orientation.h5") - file_calibration = join(dir_data_sofast, "data_measurement/image_calibration.h5") - file_facet = join(dir_data_common, "Facet_NSTTF.json") - - # Or, optionally, process high-resolution SOFAST sample data by uncommenting the lines below - # - # dir_data_sofast = 'path/to/sample_data/sofast/sandia_lab/sofast_fringe' - # dir_data_common = 'path/to/sample_data/sofast/sandia_lab/sofast_common' - # file_measurement = join(dir_data_sofast, 'data_measurement/facet_landscape_rectangular.h5') - # file_camera = join(dir_data_common, 'camera_sofast_optics_lab_landscape.h5') - # file_display = join(dir_data_common, 'display_shape_optics_lab_landscape_rectangular_distorted_2d_11x11.h5') - # file_orientation = join(dir_data_common, 'spatial_orientation_optics_lab_landscape.h5') - # file_calibration = join( - # dir_data_sofast, 'data_measurement/image_calibration_scaling_nominal_optics_lab_landscape.h5' - # ) - # file_facet = join(dir_data_common, 'facet_NSTTF.json') - # 1. Load saved single facet Sofast collection data # ================================================= camera = Camera.load_from_hdf(file_camera) @@ -209,5 +195,125 @@ def example_process_single_facet(): plots.plot() +def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, verbose_param=None): + """ + Sets up and runs the example_process_single_facet() routine. + + Parameters + ---------- + + arg_settings_dir_body_ext : str + Full path and filename for settings file, containing inputand output directories, plot control settings, etc. + Optional. If not provided, internal defaults are used. + See code for options sought within file. + + verbose : bool + If true, output detailed progress and calculation output. + """ + # Get settings + if arg_settings_dir_body_ext is None: + print("Using default control settings.") + # Verbose control + if verbose_param is None: + verbose = False + else: + verbose = verbose_param + # Define sample data directories + dir_data_sofast = join(opencsp_code_dir(), "test/data/sofast_fringe") + dir_data_common = join(opencsp_code_dir(), "test/data/sofast_common") + # Input files + file_camera = join(dir_data_common, "camera_sofast_downsampled.h5") + file_display = join(dir_data_common, "display_distorted_2d.h5") + file_orientation = join(dir_data_common, "spatial_orientation.h5") + file_facet = join(dir_data_common, "Facet_NSTTF.json") + file_calibration = join(dir_data_sofast, "data_measurement/image_calibration.h5") + file_measurement = join(dir_data_sofast, "data_measurement/measurement_facet.h5") + # Define save dir + dir_save = join(dirname(__file__), "data/output/single_facet") + + else: + print("Loading control from settings file:", arg_settings_dir_body_ext) + if not ft.file_exists(arg_settings_dir_body_ext): + print("ERROR: In " + basename(__file__) + ", settings file does not exist. Settings file:") + print(" ", arg_settings_dir_body_ext) + assert False + settings = configparser.ConfigParser() + settings.read(arg_settings_dir_body_ext) + # Verbose control + verbose_setting = settings["Default"]["verbose"] + if verbose_param is None: + verbose = verbose_setting + else: + verbose = verbose_param + # Input files + file_camera = settings["Default"]["file_camera"] + file_display = settings["Default"]["file_display"] + file_orientation = settings["Default"]["file_orientation"] + file_facet = settings["Default"]["file_facet"] + file_calibration = settings["Default"]["file_calibration"] + file_measurement = settings["Default"]["file_measurement"] + # Define save dir + dir_save = settings["Default"]["dir_save"] + + # Ensure output directory is ready + ft.create_directories_if_necessary(dir_save) + + # Set up logger + logfile_dir_body_ext = join(dir_save, splitext(basename(__file__))[0] + '_log.txt') + print("logfile_dir_body_ext = ", logfile_dir_body_ext) + lt.logger(logfile_dir_body_ext, lt.log.INFO) + # Output standard lines. + if verbose: + lt.info_strings_from_file(join(dirname(__file__), splitext(basename(__file__))[0] + '_README.md')) + lt.info('Starting program ' + __file__) + lt.info('verbose = ' + str(verbose)) + lt.info('file_camera = ' + str(file_camera)) + lt.info('file_display = ' + str(file_display)) + lt.info('file_orientation = ' + str(file_orientation)) + lt.info('file_facet = ' + str(file_facet)) + lt.info('file_calibration = ' + str(file_calibration)) + lt.info('file_measurement = ' + str(file_measurement)) + lt.info('dir_save = ' + str(dir_save)) + if verbose: + lt.info('Calling routine example_process_single_facet(...)...') + example_process_single_facet( + verbose, file_camera, file_display, file_orientation, file_facet, file_calibration, file_measurement, dir_save + ) + + if __name__ == "__main__": - example_process_single_facet() + # Parse command-line arguments, if any. + # Execute "python .py --help" to see usage tips. + # + # Source - https://stackoverflow.com/a + # Posted by Martijn Pieters, modified by community. See post 'Timeline' for change history + # Retrieved 2025-12-04, License - CC BY-SA 4.0 + parser = argparse.ArgumentParser( + prog=__file__.rstrip(".py"), + description='Analyze SOFAST measurement of a single facet, image processing, fitting, and producing analysis plots. See "example_process_single_facet_README.md" for details.', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "-s", + "--settings_dir_body_ext", + required=False, + dest="settings_dir_body_ext", + default=None, + help="Settings file defining run parameters (input/output directories, etc).", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + dest="verbose", + help="Output detailed information reporting run progress and calculations.", + ) + args = parser.parse_args() + arg_settings_dir_body_ext: str = args.settings_dir_body_ext + verbose: bool = args.verbose + + # Manual override for use when debugging. Comment this line for normal runs. + # verbose: bool = True + + # Call driver. + example_process_single_facet_driver(arg_settings_dir_body_ext, verbose) diff --git a/example/sofast_fringe/example_process_single_facet_README.md b/example/sofast_fringe/example_process_single_facet_README.md new file mode 100644 index 000000000..69c4b82a5 --- /dev/null +++ b/example/sofast_fringe/example_process_single_facet_README.md @@ -0,0 +1,66 @@ +example_scene_reconstruction_README.txt: +======================================== +Example scene reconstruction calculation. Given photos with Aruco markers, find marker +and camera 3-d positions. See "example_scene_reconstruction_README.txt" for details. + +To run this as a pytest on the built-in input: + 1. cd to the OpenCSP code directory. + 2. cd to the example subdirectory. + 3. Execute pytest: + pytest + or + pytest scene_reconstruction\example_scene_reconstruction.py + +To run this on the default built-in input, omit the -s option: + 1. cd to the directory containing the script "example_scene_reconstruction.py". + 2. Run the script: + python example_scene_reconstruction.py --verbose + The "--verbose" flag is optional.' + +To run this on new input, use the -s option and point to a settings control file. +For an example settings file, see: + \example\scene_reconstruction\example_scene_reconstruction_settings_ctemp.ini + +We recommend copying this file and placing it alongside the data you wish to run. +For example, to run the full-size OpenCSP example:' + 1. Create a directory "C:\ctemp\OpenCSP_ctemp\example_data_large" for holding example data. + 2. Create subdirectory "scene_reconstruction" + 3. Create subsubdirectory "input", and fill it with the required input files: + "alignment_points.csv" + "camera.h5" + "known_point_locations.csv" + "point_pair_distances.csv" + Subdirectory "aruco_marker_images" containing images of scene with Aruco markers. + 4. Copy the file "example_scene_reconstruction_settings_ctemp.ini" into the subdirectory + "scene_reconstruction" made in step 2.' + 5. Launch a PowerShell and ensure the OpenCSP virtual environment is activated. + 6. cd to the directory containing the script "example_scene_reconstruction.py". + 7. Run the script, providing the -s option and pointing to the .ini file: + python example_scene_reconstruction.py --verbose -s "C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\example_scene_reconstruction_settings_ctemp.ini" + 8. The "--verbose" option generates additional status and calculation output. + 9. The output will be written to an "example_scene_reconstruction_output" subdirectory + created alongside the input directory. + (This is to distinguish it from output from other examples within the directory.) + +To run this calculation on your own data: + A. Create a directory holding your input data. + B. Copy the "example_scene_reconstruction_settings_ctemp.ini" file to a new name, such as + "My_Data_scene_reconstruction_settings.ini" and edit it to point to your data location. + C. For the sake of example, suppose you place your data in "C:\ctemp\OpenCSP_ctemp\MyData", + and also suppose you place your new "My_Data_scene_reconstruction_settings.ini" file + in this directory. + Then you can run the same calculation on your data by: + a. cd to the directory containing the script "example_scene_reconstruction.py". + b. Run the script, providing the -s option and pointing to your .ini file: + python example_scene_reconstruction.py --verbose -s "C:\ctemp\OpenCSP_ctemp\MyData\My_Data_scene_reconstruction_settings_ctemp.ini" + D. The output will be written to the output subdirectory you specify in your .ini file.' + +For a detailed description of the algorithm and its input and output, see: + B. J. Smith, R. C. Brost, and B. G. Bean. + Scene Reconstruction User Guide, Document Version 1.0. + Sandia National Laboratories Report SAND2024-10625, August 2024. + https://doi.org/10.2172/2463024 + +Also available through OpenCSP_Documents; see https://opencsp.sandia.gov + +======================================== From bf061d7f476ef73e414f7b534cceec76975c60eb Mon Sep 17 00:00:00 2001 From: rcbrost Date: Wed, 21 Jan 2026 09:26:49 -0700 Subject: [PATCH 19/29] Fixed bug in example_process_single_facet.py causing pytest failure. Fixed example_process_single_facet_README.txt file. --- .../example_process_single_facet.py | 4 +- .../example_process_single_facet_README.md | 37 ++++++++++--------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/example/sofast_fringe/example_process_single_facet.py b/example/sofast_fringe/example_process_single_facet.py index 04154dc6d..042dbed58 100644 --- a/example/sofast_fringe/example_process_single_facet.py +++ b/example/sofast_fringe/example_process_single_facet.py @@ -54,7 +54,7 @@ import opencsp.common.lib.tool.log_tools as lt -def example_process_single_facet( +def process_single_facet( verbose: bool, file_camera: str, file_display: str, @@ -276,7 +276,7 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v lt.info('dir_save = ' + str(dir_save)) if verbose: lt.info('Calling routine example_process_single_facet(...)...') - example_process_single_facet( + process_single_facet( verbose, file_camera, file_display, file_orientation, file_facet, file_calibration, file_measurement, dir_save ) diff --git a/example/sofast_fringe/example_process_single_facet_README.md b/example/sofast_fringe/example_process_single_facet_README.md index 69c4b82a5..ef94b7e6f 100644 --- a/example/sofast_fringe/example_process_single_facet_README.md +++ b/example/sofast_fringe/example_process_single_facet_README.md @@ -1,7 +1,8 @@ -example_scene_reconstruction_README.txt: +example_process_single_facet_README.txt: ======================================== -Example scene reconstruction calculation. Given photos with Aruco markers, find marker -and camera 3-d positions. See "example_scene_reconstruction_README.txt" for details. +Example SOFAST data processing for a single facet measurement. +Given a stored measurement file from a SOFAST data acquisition, process the file to +construct a slope map and generate the standard plot output suite. To run this as a pytest on the built-in input: 1. cd to the OpenCSP code directory. @@ -9,50 +10,50 @@ To run this as a pytest on the built-in input: 3. Execute pytest: pytest or - pytest scene_reconstruction\example_scene_reconstruction.py + pytest sofast_fringe\example_process_single_facet.py To run this on the default built-in input, omit the -s option: - 1. cd to the directory containing the script "example_scene_reconstruction.py". + 1. cd to the directory containing the script "example_process_single_facet.py". 2. Run the script: - python example_scene_reconstruction.py --verbose + python example_process_single_facet.py --verbose The "--verbose" flag is optional.' To run this on new input, use the -s option and point to a settings control file. For an example settings file, see: - \example\scene_reconstruction\example_scene_reconstruction_settings_ctemp.ini + \example\process_single_facet\example_process_single_facet_settings_ctemp.ini We recommend copying this file and placing it alongside the data you wish to run. For example, to run the full-size OpenCSP example:' 1. Create a directory "C:\ctemp\OpenCSP_ctemp\example_data_large" for holding example data. - 2. Create subdirectory "scene_reconstruction" + 2. Create subdirectory "process_single_facet" 3. Create subsubdirectory "input", and fill it with the required input files: "alignment_points.csv" "camera.h5" "known_point_locations.csv" "point_pair_distances.csv" Subdirectory "aruco_marker_images" containing images of scene with Aruco markers. - 4. Copy the file "example_scene_reconstruction_settings_ctemp.ini" into the subdirectory - "scene_reconstruction" made in step 2.' + 4. Copy the file "example_process_single_facet_settings_ctemp.ini" into the subdirectory + "process_single_facet" made in step 2.' 5. Launch a PowerShell and ensure the OpenCSP virtual environment is activated. - 6. cd to the directory containing the script "example_scene_reconstruction.py". + 6. cd to the directory containing the script "example_process_single_facet.py". 7. Run the script, providing the -s option and pointing to the .ini file: - python example_scene_reconstruction.py --verbose -s "C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\example_scene_reconstruction_settings_ctemp.ini" + python example_process_single_facet.py --verbose -s "C:\ctemp\OpenCSP_ctemp\example_data_large\process_single_facet\example_process_single_facet_settings_ctemp.ini" 8. The "--verbose" option generates additional status and calculation output. - 9. The output will be written to an "example_scene_reconstruction_output" subdirectory + 9. The output will be written to an "example_process_single_facet_output" subdirectory created alongside the input directory. (This is to distinguish it from output from other examples within the directory.) To run this calculation on your own data: A. Create a directory holding your input data. - B. Copy the "example_scene_reconstruction_settings_ctemp.ini" file to a new name, such as - "My_Data_scene_reconstruction_settings.ini" and edit it to point to your data location. + B. Copy the "example_process_single_facet_settings_ctemp.ini" file to a new name, such as + "My_Data_process_single_facet_settings.ini" and edit it to point to your data location. C. For the sake of example, suppose you place your data in "C:\ctemp\OpenCSP_ctemp\MyData", - and also suppose you place your new "My_Data_scene_reconstruction_settings.ini" file + and also suppose you place your new "My_Data_process_single_facet_settings.ini" file in this directory. Then you can run the same calculation on your data by: - a. cd to the directory containing the script "example_scene_reconstruction.py". + a. cd to the directory containing the script "example_process_single_facet.py". b. Run the script, providing the -s option and pointing to your .ini file: - python example_scene_reconstruction.py --verbose -s "C:\ctemp\OpenCSP_ctemp\MyData\My_Data_scene_reconstruction_settings_ctemp.ini" + python example_process_single_facet.py --verbose -s "C:\ctemp\OpenCSP_ctemp\MyData\My_Data_process_single_facet_settings_ctemp.ini" D. The output will be written to the output subdirectory you specify in your .ini file.' For a detailed description of the algorithm and its input and output, see: From ee7160ddae9fee26bd22533b1b8e0976d2f48c28 Mon Sep 17 00:00:00 2001 From: rcbrost Date: Wed, 21 Jan 2026 09:35:28 -0700 Subject: [PATCH 20/29] Removed warning that 'verbose' variables in subroutines overwrote 'verbose' values set in main. --- .../scene_reconstruction/example_scene_reconstruction.py | 6 +++--- example/sofast_fringe/example_process_single_facet.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/example/scene_reconstruction/example_scene_reconstruction.py b/example/scene_reconstruction/example_scene_reconstruction.py index 8de4bac3a..43978058b 100644 --- a/example/scene_reconstruction/example_scene_reconstruction.py +++ b/example/scene_reconstruction/example_scene_reconstruction.py @@ -243,11 +243,11 @@ def example_scene_reconstruction_driver(arg_settings_dir_body_ext: str = None, v help="Output detailed information reporting run progress and calculations.", ) args = parser.parse_args() - arg_settings_dir_body_ext: str = args.settings_dir_body_ext - verbose: bool = args.verbose + arg_settings_dir_body_ext_main: str = args.settings_dir_body_ext + verbose_main: bool = args.verbose # Manual override for use when debugging. Comment this line for normal runs. # verbose: bool = True # Call driver. - example_scene_reconstruction_driver(arg_settings_dir_body_ext, verbose) + example_scene_reconstruction_driver(arg_settings_dir_body_ext_main, verbose_main) diff --git a/example/sofast_fringe/example_process_single_facet.py b/example/sofast_fringe/example_process_single_facet.py index 042dbed58..97f7bc9ac 100644 --- a/example/sofast_fringe/example_process_single_facet.py +++ b/example/sofast_fringe/example_process_single_facet.py @@ -309,11 +309,11 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v help="Output detailed information reporting run progress and calculations.", ) args = parser.parse_args() - arg_settings_dir_body_ext: str = args.settings_dir_body_ext - verbose: bool = args.verbose + arg_settings_dir_body_ext_main: str = args.settings_dir_body_ext + verbose_main: bool = args.verbose # Manual override for use when debugging. Comment this line for normal runs. # verbose: bool = True # Call driver. - example_process_single_facet_driver(arg_settings_dir_body_ext, verbose) + example_process_single_facet_driver(arg_settings_dir_body_ext_main, verbose_main) From cc28c71710d3ff05d41070363b93145eb7d0e87e Mon Sep 17 00:00:00 2001 From: rcbrost Date: Wed, 21 Jan 2026 10:55:48 -0700 Subject: [PATCH 21/29] In example_process_single_facet.py, set output directories to new naming convention. --- example/sofast_fringe/example_process_single_facet.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/example/sofast_fringe/example_process_single_facet.py b/example/sofast_fringe/example_process_single_facet.py index 97f7bc9ac..e79d39fc4 100644 --- a/example/sofast_fringe/example_process_single_facet.py +++ b/example/sofast_fringe/example_process_single_facet.py @@ -94,7 +94,7 @@ def process_single_facet( # ======================================================== fringes = Fringes(measurement.fringe_periods_x, measurement.fringe_periods_y) images = fringes.get_frames(640, 320, "uint8", [0, 255]) # writes images we projected from sofast projector to disk - dir_save_cur = join(dir_save, "1_images_fringes_projected") + dir_save_cur = join(dir_save, "B1_projected_fringes") ft.create_directories_if_necessary(dir_save_cur) # Save y images for idx_image in range(measurement.num_y_ims): @@ -107,7 +107,7 @@ def process_single_facet( # 3. Save captured sinusoidal fringe images and mask images to PNG format # ======================================================================= - dir_save_cur = join(dir_save, "2_images_captured") + dir_save_cur = join(dir_save, "B2_captured_fringes") ft.create_directories_if_necessary(dir_save_cur) # Save mask (like a pixel mask value (all 0s, all 255s)) images @@ -125,7 +125,7 @@ def process_single_facet( # 4. Processes data with Sofast and save processed data to HDF5 # ============================================================= - dir_save_cur = join(dir_save, "3_processed_data") + dir_save_cur = join(dir_save, "B3_output_analysis") ft.create_directories_if_necessary(dir_save_cur) # Define surface definition (parabolic surface), this is the mirror @@ -154,7 +154,7 @@ def process_single_facet( # 5. Generate plot suite and save images files # ============================================ - dir_save_cur = join(dir_save, "4_processed_output_figures") + dir_save_cur = join(dir_save, "B4_output_figures") ft.create_directories_if_necessary(dir_save_cur) # Get measured and reference optics From c2e5c250b2bcbf71bb2ee0bb982bba1edb34c897 Mon Sep 17 00:00:00 2001 From: rcbrost Date: Wed, 21 Jan 2026 15:48:01 -0700 Subject: [PATCH 22/29] Added output file prefixes encoding measurement and process spec. --- .../example_process_single_facet.py | 110 +++++++++++------- opencsp/common/lib/csp/StandardPlotOutput.py | 26 +++-- 2 files changed, 84 insertions(+), 52 deletions(-) diff --git a/example/sofast_fringe/example_process_single_facet.py b/example/sofast_fringe/example_process_single_facet.py index e79d39fc4..55cff2015 100644 --- a/example/sofast_fringe/example_process_single_facet.py +++ b/example/sofast_fringe/example_process_single_facet.py @@ -63,6 +63,9 @@ def process_single_facet( file_calibration: str, file_measurement: str, dir_save: str, + measurement_id: str, + post_process_id: str, + plots: StandardPlotOutput, ): """Performs processing of previously collected SOFAST data of single facet mirror. @@ -78,8 +81,8 @@ def process_single_facet( # Set up save dir ft.create_directories_if_necessary(dir_save) - # Set up logger - lt.logger(join(dir_save, "log.txt"), lt.log.WARN) + # Construct output file prefix + output_file_prefix = measurement_id + "_" + post_process_id + "_" # 1. Load saved single facet Sofast collection data # ================================================= @@ -99,11 +102,11 @@ def process_single_facet( # Save y images for idx_image in range(measurement.num_y_ims): image = images[..., idx_image] - imageio.imwrite(join(dir_save_cur, f"y_{idx_image:02d}.png"), image) + imageio.imwrite(join(dir_save_cur, output_file_prefix + f"y_{idx_image:02d}.png"), image) # Save x images for idx_image in range(measurement.num_x_ims): image = images[..., measurement.num_y_ims + idx_image] - imageio.imwrite(join(dir_save_cur, f"x_{idx_image:02d}.png"), image) + imageio.imwrite(join(dir_save_cur, output_file_prefix + f"x_{idx_image:02d}.png"), image) # 3. Save captured sinusoidal fringe images and mask images to PNG format # ======================================================================= @@ -113,15 +116,15 @@ def process_single_facet( # Save mask (like a pixel mask value (all 0s, all 255s)) images for idx_image in [0, 1]: image = measurement.mask_images[..., idx_image] - imageio.imwrite(join(dir_save_cur, f"mask_{idx_image:02d}.png"), image) + imageio.imwrite(join(dir_save_cur, output_file_prefix + f"mask_{idx_image:02d}.png"), image) # Save y images (when lines were vertical, e.g.) for idx_image in range(measurement.num_y_ims): image = measurement.fringe_images_y[..., idx_image] - imageio.imwrite(join(dir_save_cur, f"y_{idx_image:02d}.png"), image) + imageio.imwrite(join(dir_save_cur, output_file_prefix + f"y_{idx_image:02d}.png"), image) # Save x images (when lines were horizontal, e.g.) for idx_image in range(measurement.num_x_ims): image = measurement.fringe_images_x[..., idx_image] - imageio.imwrite(join(dir_save_cur, f"x_{idx_image:02d}.png"), image) + imageio.imwrite(join(dir_save_cur, output_file_prefix + f"x_{idx_image:02d}.png"), image) # 4. Processes data with Sofast and save processed data to HDF5 # ============================================================= @@ -140,16 +143,16 @@ def process_single_facet( # Process sofast.process_optic_singlefacet(facet_data, surface) - # Save processed data to HDF5 format - sofast.save_to_hdf(join(dir_save_cur, "data_sofast_processed.h5")) - - # Save measurement statistics to JSON + # Get measurement statistics config = SofastConfiguration() config.load_sofast_object(sofast) measurement_stats = config.get_measurement_stats() + # Save processed data to HDF5 format + sofast.save_to_hdf(join(dir_save_cur, output_file_prefix + "data_sofast_processed.h5")) + # Save measurement stats as JSON - with open(join(dir_save_cur, "measurement_statistics.json"), "w", encoding="utf-8") as f: + with open(join(dir_save_cur, output_file_prefix + "measurement_statistics.json"), "w", encoding="utf-8") as f: json.dump(measurement_stats, f, indent=3) # 5. Generate plot suite and save images files @@ -161,35 +164,11 @@ def process_single_facet( mirror_measured = sofast.get_optic().mirror.no_parent_copy() mirror_reference = MirrorParametric.generate_symmetric_paraboloid(100, mirror_measured.region) - # Define viewing/illumination geometry - v_target_center = Vxyz((0, 0, 100)) - v_target_normal = Vxyz((0, 0, -1)) - source = LightSourceSun.from_given_sun_position(Uxyz((0, 0, -1)), resolution=40) - - # Save optic objects - plots = StandardPlotOutput() + # Save optic objects and output destination plots.optic_measured = mirror_measured plots.optic_reference = mirror_reference - - # Update visualization parameters - plots.options_slope_vis.clim = 7 - plots.options_slope_vis.resolution = 0.001 - - plots.options_slope_deviation_vis.clim = 1.5 - plots.options_slope_deviation_vis.resolution = 0.001 - - plots.options_curvature_vis.resolution = 0.001 - - plots.options_ray_trace_vis.enclosed_energy_max_semi_width = 1 - - plots.options_file_output.to_save = True - plots.options_file_output.number_in_name = False plots.options_file_output.output_dir = dir_save_cur - - # Define ray trace parameters - plots.params_ray_trace.source = source - plots.params_ray_trace.v_target_center = v_target_center - plots.params_ray_trace.v_target_normal = v_target_normal + plots.options_file_output.file_prefix = output_file_prefix # Create standard output plots plots.plot() @@ -230,6 +209,9 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v file_measurement = join(dir_data_sofast, "data_measurement/measurement_facet.h5") # Define save dir dir_save = join(dirname(__file__), "data/output/single_facet") + # Strings denoting computation. + measurement_id = "Time_Mirror_InstrumentMode" + post_process_id = "PostSpec" else: print("Loading control from settings file:", arg_settings_dir_body_ext) @@ -254,15 +236,20 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v file_measurement = settings["Default"]["file_measurement"] # Define save dir dir_save = settings["Default"]["dir_save"] + # Strings denoting computation. + measurement_id = settings["Default"]["measurement_id"] + post_process_id = settings["Default"]["post_process_id"] # Ensure output directory is ready ft.create_directories_if_necessary(dir_save) # Set up logger - logfile_dir_body_ext = join(dir_save, splitext(basename(__file__))[0] + '_log.txt') + logfile_dir_body_ext = join( + dir_save, measurement_id + "_" + post_process_id + "_" + splitext(basename(__file__))[0] + '_log.txt' + ) print("logfile_dir_body_ext = ", logfile_dir_body_ext) lt.logger(logfile_dir_body_ext, lt.log.INFO) - # Output standard lines. + # Output standard lines if verbose: lt.info_strings_from_file(join(dirname(__file__), splitext(basename(__file__))[0] + '_README.md')) lt.info('Starting program ' + __file__) @@ -274,10 +261,51 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v lt.info('file_calibration = ' + str(file_calibration)) lt.info('file_measurement = ' + str(file_measurement)) lt.info('dir_save = ' + str(dir_save)) + lt.info('measurement_id = ' + str(measurement_id)) + lt.info('post_process_id = ' + str(post_process_id)) if verbose: lt.info('Calling routine example_process_single_facet(...)...') + + # Define viewing/illumination geometry + v_target_center = Vxyz((0, 0, 100)) + v_target_normal = Vxyz((0, 0, -1)) + source = LightSourceSun.from_given_sun_position(Uxyz((0, 0, -1)), resolution=40) + + # Setup plot control + plots = StandardPlotOutput() + + # Update visualization parameters + plots.options_slope_vis.clim = 7 + plots.options_slope_vis.resolution = 0.001 + + plots.options_slope_deviation_vis.clim = 1.5 + plots.options_slope_deviation_vis.resolution = 0.001 + + plots.options_curvature_vis.resolution = 0.001 + + plots.options_ray_trace_vis.enclosed_energy_max_semi_width = 1 + + plots.options_file_output.to_save = True + plots.options_file_output.number_in_name = False + + # Define ray trace parameters + plots.params_ray_trace.source = source + plots.params_ray_trace.v_target_center = v_target_center + plots.params_ray_trace.v_target_normal = v_target_normal + + # Process and output process_single_facet( - verbose, file_camera, file_display, file_orientation, file_facet, file_calibration, file_measurement, dir_save + verbose, + file_camera, + file_display, + file_orientation, + file_facet, + file_calibration, + file_measurement, + dir_save, + measurement_id, + post_process_id, + plots, ) diff --git a/opencsp/common/lib/csp/StandardPlotOutput.py b/opencsp/common/lib/csp/StandardPlotOutput.py index 38616b1cb..eb62a103d 100644 --- a/opencsp/common/lib/csp/StandardPlotOutput.py +++ b/opencsp/common/lib/csp/StandardPlotOutput.py @@ -114,6 +114,8 @@ class _OptionsFileOutput: """To close figures after save. (default False)""" number_in_name: bool = True """To keep figure number in save name. (default True)""" + file_prefix: str = None + """String to prefix each output file, including separator""" @dataclass @@ -270,7 +272,7 @@ def _plot_slope_deviation(self): fig_rec = fm.setup_figure( self.fig_control, self.axis_control, - name="Slope Deviation Magnitude", + name=self.options_file_output.file_prefix + "Slope Deviation Magnitude", number_in_name=self.options_file_output.number_in_name, ) self.optic_measured.plot_orthorectified_slope_error( @@ -295,7 +297,7 @@ def _plot_slope_deviation(self): fig_rec = fm.setup_figure( self.fig_control, self.axis_control, - name="Slope Deviation X", + name=self.options_file_output.file_prefix + "Slope Deviation X", number_in_name=self.options_file_output.number_in_name, ) self.optic_measured.plot_orthorectified_slope_error( @@ -320,7 +322,7 @@ def _plot_slope_deviation(self): fig_rec = fm.setup_figure( self.fig_control, self.axis_control, - name="Slope Deviation Y", + name=self.options_file_output.file_prefix + "Slope Deviation Y", number_in_name=self.options_file_output.number_in_name, ) self.optic_measured.plot_orthorectified_slope_error( @@ -366,7 +368,9 @@ def _plot_enclosed_energy(self): # Make figure fig_rec = fm.setup_figure( - self.fig_control, name='Ensquared Energy', number_in_name=self.options_file_output.number_in_name + self.fig_control, + name=self.options_file_output.file_prefix + 'Ensquared Energy', + number_in_name=self.options_file_output.number_in_name, ) # Draw reference if available @@ -430,7 +434,7 @@ def _plot_curvature(self, optic: MirrorAbstract, suffix: str): fig_rec = fm.setup_figure( self.fig_control, self.axis_control, - name='Curvature Combined ' + suffix, + name=self.options_file_output.file_prefix + 'Curvature Combined ' + suffix, number_in_name=self.options_file_output.number_in_name, ) optic.plot_orthorectified_curvature( @@ -453,7 +457,7 @@ def _plot_curvature(self, optic: MirrorAbstract, suffix: str): fig_rec = fm.setup_figure( self.fig_control, self.axis_control, - name='Curvature X ' + suffix, + name=self.options_file_output.file_prefix + 'Curvature X ' + suffix, number_in_name=self.options_file_output.number_in_name, ) optic.plot_orthorectified_curvature( @@ -476,7 +480,7 @@ def _plot_curvature(self, optic: MirrorAbstract, suffix: str): fig_rec = fm.setup_figure( self.fig_control, self.axis_control, - name='Curvature Y ' + suffix, + name=self.options_file_output.file_prefix + 'Curvature Y ' + suffix, number_in_name=self.options_file_output.number_in_name, ) optic.plot_orthorectified_curvature( @@ -505,7 +509,7 @@ def _plot_slope(self, optic: MirrorAbstract, suffix: str): fig_rec = fm.setup_figure( self.fig_control, self.axis_control, - name="Slope Magnitude " + suffix, + name=self.options_file_output.file_prefix + "Slope Magnitude " + suffix, number_in_name=self.options_file_output.number_in_name, ) optic.plot_orthorectified_slope( @@ -529,7 +533,7 @@ def _plot_slope(self, optic: MirrorAbstract, suffix: str): fig_rec = fm.setup_figure( self.fig_control, self.axis_control, - name="Slope X " + suffix, + name=self.options_file_output.file_prefix + "Slope X " + suffix, number_in_name=self.options_file_output.number_in_name, ) optic.plot_orthorectified_slope( @@ -553,7 +557,7 @@ def _plot_slope(self, optic: MirrorAbstract, suffix: str): fig_rec = fm.setup_figure( self.fig_control, self.axis_control, - name="Slope Y " + suffix, + name=self.options_file_output.file_prefix + "Slope Y " + suffix, number_in_name=self.options_file_output.number_in_name, ) optic.plot_orthorectified_slope( @@ -578,7 +582,7 @@ def _plot_ray_trace_image(self, ray_trace_data: _RayTraceOutput, suffix: str): fig_rec = fm.setup_figure( self.fig_control, self.axis_control, - name='Ray Trace Image ' + suffix, + name=self.options_file_output.file_prefix + 'Ray Trace Image ' + suffix, number_in_name=self.options_file_output.number_in_name, ) fig_rec.axis.imshow( From 594ffdebd862b68d2d067049444616f5920deece Mon Sep 17 00:00:00 2001 From: rcbrost Date: Thu, 22 Jan 2026 08:44:04 -0700 Subject: [PATCH 23/29] Set several standard plot control fields from input configuration file. Included checks for missing parameters, true/false strings, contiguous tokens. --- .../example_process_single_facet.py | 58 +++++++------ opencsp/common/lib/csp/StandardPlotOutput.py | 84 ++++++++++++++++++- opencsp/common/lib/tool/string_tools.py | 50 ++++++++++- 3 files changed, 160 insertions(+), 32 deletions(-) diff --git a/example/sofast_fringe/example_process_single_facet.py b/example/sofast_fringe/example_process_single_facet.py index 55cff2015..d9b5140a4 100644 --- a/example/sofast_fringe/example_process_single_facet.py +++ b/example/sofast_fringe/example_process_single_facet.py @@ -52,6 +52,7 @@ from opencsp.common.lib.opencsp_path.opencsp_root_path import opencsp_code_dir import opencsp.common.lib.tool.file_tools as ft import opencsp.common.lib.tool.log_tools as lt +import opencsp.common.lib.tool.string_tools as st def process_single_facet( @@ -189,6 +190,9 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v verbose : bool If true, output detailed progress and calculation output. """ + # Setup plot control, whcih might have some values set from settings, if provided. + plots = StandardPlotOutput() + # Get settings if arg_settings_dir_body_ext is None: print("Using default control settings.") @@ -212,6 +216,25 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v # Strings denoting computation. measurement_id = "Time_Mirror_InstrumentMode" post_process_id = "PostSpec" + # Set plot control parameters to the values we want for the default example. + plots.options_slope_vis.clim = 7 + plots.options_slope_vis.resolution = 0.001 + plots.options_slope_deviation_vis.clim = 1.5 + plots.options_slope_deviation_vis.resolution = 0.001 + plots.options_curvature_vis.resolution = 0.001 + plots.options_ray_trace_vis.enclosed_energy_max_semi_width = 1 + plots.options_file_output.to_save = True + plots.options_file_output.number_in_name = False + + # Define viewing/illumination geometry + v_target_center = Vxyz((0, 0, 100)) + v_target_normal = Vxyz((0, 0, -1)) + source = LightSourceSun.from_given_sun_position(Uxyz((0, 0, -1)), resolution=40) + + # Define ray trace parameters + plots.params_ray_trace.source = source + plots.params_ray_trace.v_target_center = v_target_center + plots.params_ray_trace.v_target_normal = v_target_normal else: print("Loading control from settings file:", arg_settings_dir_body_ext) @@ -236,9 +259,11 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v file_measurement = settings["Default"]["file_measurement"] # Define save dir dir_save = settings["Default"]["dir_save"] - # Strings denoting computation. - measurement_id = settings["Default"]["measurement_id"] - post_process_id = settings["Default"]["post_process_id"] + # Strings denoting computation + measurement_id = st.verify_contiguous(settings["Default"]["measurement_id"]) + post_process_id = st.verify_contiguous(settings["Default"]["post_process_id"]) + # Set plot control parameters + plots.set_plot_control_from_settings(settings) # Ensure output directory is ready ft.create_directories_if_necessary(dir_save) @@ -266,33 +291,6 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v if verbose: lt.info('Calling routine example_process_single_facet(...)...') - # Define viewing/illumination geometry - v_target_center = Vxyz((0, 0, 100)) - v_target_normal = Vxyz((0, 0, -1)) - source = LightSourceSun.from_given_sun_position(Uxyz((0, 0, -1)), resolution=40) - - # Setup plot control - plots = StandardPlotOutput() - - # Update visualization parameters - plots.options_slope_vis.clim = 7 - plots.options_slope_vis.resolution = 0.001 - - plots.options_slope_deviation_vis.clim = 1.5 - plots.options_slope_deviation_vis.resolution = 0.001 - - plots.options_curvature_vis.resolution = 0.001 - - plots.options_ray_trace_vis.enclosed_energy_max_semi_width = 1 - - plots.options_file_output.to_save = True - plots.options_file_output.number_in_name = False - - # Define ray trace parameters - plots.params_ray_trace.source = source - plots.params_ray_trace.v_target_center = v_target_center - plots.params_ray_trace.v_target_normal = v_target_normal - # Process and output process_single_facet( verbose, diff --git a/opencsp/common/lib/csp/StandardPlotOutput.py b/opencsp/common/lib/csp/StandardPlotOutput.py index eb62a103d..519c9f2ad 100644 --- a/opencsp/common/lib/csp/StandardPlotOutput.py +++ b/opencsp/common/lib/csp/StandardPlotOutput.py @@ -1,7 +1,7 @@ """Class used to display/save the suite of standard output plots after measuring a CSP Optic object.""" +import configparser from dataclasses import dataclass, field - import numpy as np import opencsp.common.lib.render_control.RenderControlAxis as rca @@ -17,6 +17,7 @@ import opencsp.common.lib.render.figure_management as fm import opencsp.common.lib.render_control.RenderControlEnclosedEnergy as rcee import opencsp.common.lib.tool.log_tools as lt +import opencsp.common.lib.tool.string_tools as st @dataclass @@ -185,6 +186,87 @@ def _has_measured_ray_trace(self) -> bool: def _has_reference_ray_trace(self) -> bool: return self._ray_trace_output_reference is not None + def set_plot_control_from_settings(self, settings: configparser.ConfigParser): + """ + Fills in plot control fields that are provided in settings. + If the settings does not contain a key, then the corresponding plot control field is left unchanged. + """ + + # File output control. + if ("Default" in settings) and ("plots.options_file_output.to_save" in settings["Default"]): + self.options_file_output.to_save = st.convert_true_false_string_to_boolean( + settings["Default"]["plots.options_file_output.to_save"] + ) + if ("Default" in settings) and ("plots.options_file_output.number_in_name" in settings["Default"]): + self.options_file_output.number_in_name = st.convert_true_false_string_to_boolean( + settings["Default"]["plots.options_file_output.number_in_name"] + ) + + # Slope plot control. + if ("Default" in settings) and ("plots.options_slope_vis.clim" in settings["Default"]): + self.options_slope_vis.clim = float(settings["Default"]["plots.options_slope_vis.clim"]) + if ("Default" in settings) and ("plots.options_slope_vis.resolution" in settings["Default"]): + self.options_slope_vis.resolution = float(settings["Default"]["plots.options_slope_vis.resolution"]) + + # Slope deviation plot control. + if ("Default" in settings) and ("plots.options_slope_deviation_vis.clim" in settings["Default"]): + self.options_slope_deviation_vis.clim = float(settings["Default"]["plots.options_slope_deviation_vis.clim"]) + if ("Default" in settings) and ("plots.options_slope_deviation_vis.resolution" in settings["Default"]): + self.options_slope_deviation_vis.resolution = float( + settings["Default"]["plots.options_slope_deviation_vis.resolution"] + ) + + # Curvature plot control. + if ("Default" in settings) and ("plots.options_curvature_vis.resolution" in settings["Default"]): + self.options_curvature_vis.resolution = float(settings["Default"]["plots.options_curvature_vis.resolution"]) + + # Ray trace control. + # Light source. + if ( + ("Default" in settings) + and ("plots.options_ray_trace_vis.sun_direction_x" in settings["Default"]) + and ("plots.options_ray_trace_vis.sun_direction_y" in settings["Default"]) + and ("plots.options_ray_trace_vis.sun_direction_z" in settings["Default"]) + and ("plots.options_ray_trace_vis.sun_sample_resolution" in settings["Default"]) + ): + sun_direction_x = float(settings["Default"]["plots.options_ray_trace_vis.sun_direction_x"]) + sun_direction_y = float(settings["Default"]["plots.options_ray_trace_vis.sun_direction_y"]) + sun_direction_z = float(settings["Default"]["plots.options_ray_trace_vis.sun_direction_z"]) + sun_direction = Uxyz((sun_direction_x, sun_direction_y, sun_direction_z)) + sun_sample_resolution = int(settings["Default"]["plots.options_ray_trace_vis.sun_sample_resolution"]) + self.params_ray_trace.source = LightSourceSun.from_given_sun_position( + sun_direction, resolution=sun_sample_resolution + ) + # Target center. + if ( + ("Default" in settings) + and ("plots.options_ray_trace_vis.v_target_center_x" in settings["Default"]) + and ("plots.options_ray_trace_vis.v_target_center_y" in settings["Default"]) + and ("plots.options_ray_trace_vis.v_target_center_z" in settings["Default"]) + ): + v_target_center_x = float(settings["Default"]["plots.options_ray_trace_vis.v_target_center_x"]) + v_target_center_y = float(settings["Default"]["plots.options_ray_trace_vis.v_target_center_y"]) + v_target_center_z = float(settings["Default"]["plots.options_ray_trace_vis.v_target_center_z"]) + self.params_ray_trace.v_target_center = Vxyz((v_target_center_x, v_target_center_y, v_target_center_z)) + # Target normal. + if ( + ("Default" in settings) + and ("plots.options_ray_trace_vis.v_target_normal_x" in settings["Default"]) + and ("plots.options_ray_trace_vis.v_target_normal_y" in settings["Default"]) + and ("plots.options_ray_trace_vis.v_target_normal_z" in settings["Default"]) + ): + v_target_normal_x = float(settings["Default"]["plots.options_ray_trace_vis.v_target_normal_x"]) + v_target_normal_y = float(settings["Default"]["plots.options_ray_trace_vis.v_target_normal_y"]) + v_target_normal_z = float(settings["Default"]["plots.options_ray_trace_vis.v_target_normal_z"]) + self.params_ray_trace.v_target_normal = Vxyz((v_target_normal_x, v_target_normal_y, v_target_normal_z)) + # Enclosed energy plot semi-width. + if ("Default" in settings) and ( + "plots.options_ray_trace_vis.enclosed_energy_max_semi_width" in settings["Default"] + ): + self.options_ray_trace_vis.enclosed_energy_max_semi_width = float( + settings["Default"]["plots.options_ray_trace_vis.enclosed_energy_max_semi_width"] + ) + def plot(self): """Creates standard output plot suite""" # This function checks if plotting is turned on diff --git a/opencsp/common/lib/tool/string_tools.py b/opencsp/common/lib/tool/string_tools.py index 15a1ffd37..95cdeba5f 100644 --- a/opencsp/common/lib/tool/string_tools.py +++ b/opencsp/common/lib/tool/string_tools.py @@ -4,6 +4,8 @@ import re +import opencsp.common.lib.tool.log_tools as lt + def add_to_last_sentence(base: str, add: str) -> str: """ @@ -31,7 +33,7 @@ def camel_case_split(to_split: str) -> list[str]: """ Splits the given string into pieces at leading uppercase letters. - For example:: + For example: camel_case_split("TheABCsOfPython") # ['The', 'ABCs', 'Of', 'Python'] @@ -47,3 +49,49 @@ def camel_case_split(to_split: str) -> list[str]: The to_split string, split into camel case sections. """ return re.findall(r'([a-z]+|[A-Z]+[^A-Z]+)', to_split) + + +def convert_true_false_string_to_boolean(true_or_false_str: str) -> bool: + """ + Accepts string with value either "True" or "False" and returns corresponding Boolean value. + Throws an error if not one of these two values. + + Parameters + ---------- + true_or_false_str : str + The string to convert to Boolean. Must be "True" or "False". + + Returns + ------- + bool + The corresponding value as a Boolean type. + """ + if true_or_false_str == "True": + return True + elif true_or_false_str == "False": + return False + else: + lt.error_and_raise(ValueError, f'True/False string {true_or_false_str} is not "True" or "False"') + + +def verify_contiguous(token_str: str) -> str: + """ + Checks string to ensure that is a contiguous string with no white space, newlines, etc. + Throws an error if any white space is found. + + Parameters + ---------- + token_str : str + The string to check. + + Returns + ------- + str + The input string, assuming it passes the check. + (Throws an error if not.) + """ + if token_str != token_str.strip(): + lt.error_and_raise(ValueError, f'Token string {token_str} contains leading and/or trailing whitespace.') + if len(token_str.split()) != 1: + lt.error_and_raise(ValueError, f'Token string {token_str} contains internal whitespace.') + return token_str From 98542f1030f5f6f5227e6e7b467621daa96302f5 Mon Sep 17 00:00:00 2001 From: rcbrost Date: Thu, 22 Jan 2026 12:02:18 -0700 Subject: [PATCH 24/29] Added all SOFAST plot control flags to process_example_single_facet.py. Moved .py files and two example settings configuration files into example/sofast_fringe/single_facet directory. --- ...efault_process_single_facet_settings_Q.ini | 161 ++++++++++++++++++ ...2_fast_process_single_facet_settings_Q.ini | 161 ++++++++++++++++++ .../example_process_single_facet.py | 2 +- .../example_process_single_facet_README.md | 0 opencsp/common/lib/csp/StandardPlotOutput.py | 159 +++++++++++++---- 5 files changed, 451 insertions(+), 32 deletions(-) create mode 100644 example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini create mode 100644 example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini rename example/sofast_fringe/{ => single_facet}/example_process_single_facet.py (97%) rename example/sofast_fringe/{ => single_facet}/example_process_single_facet_README.md (100%) diff --git a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini new file mode 100644 index 000000000..de262fcc8 --- /dev/null +++ b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini @@ -0,0 +1,161 @@ +# This file provides the data paths and control parameters for the example_process_single_facet.py example. +# +# To use this file, copy it to a new filename and update the values below to point at your data source. +# Original file: +# /example/sofast_fringe/example_process_single_facet_Q.ini +# +# To run the standard example using the values within this file, create a directory heirarchy: +# +# Directory containing mirror measurement results +# Q:\Mirrors\ +# +# Results for Sandia Tower Heliostat Facets +# Q:\Mirrors\SNLTF\ +# +# Results for Sandia Tower Heliostat Facet "A" +# Q:\Mirrors\SNLTF\SNLTF-A\ +# +# Measurement of facet "A" with the Optics Lab SOFAST Landscape setup, measuring in fringe mode, square image projection, white screen +# Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\ +# +# Directory containing SOFAST calibration files, constant and applicable to all measurement modes. +# Q:\Instruments\OLSL_OpticsLabSofastLandscape\ +# Contains: +# camera_sofast_optics_lab_landscape_2025_02.h5 # Camera lens calibration +# spatial_orientation_optics_lab_landscape.h5 # Relative positions of camera and screen +# +# Directory containing SOFAST calibration files, constant and used by the fringe measurement mode, square projection, white screen. +# Q:\Instruments\OLSL_OpticsLabSofastLandscape\OLSLrsqw_OpticsLabSofastLandscapeFringeSquareWhite\ +# Contains: +# display_shape_optics_lab_landscape_square_distorted_3d_100x100.h5 # Screen 3-d shape (waves, ripples,...) +# +# Directory containing files defining the mirror to measure. +# Q:\Mirrors\SNLTF\0_Common\Facet_NSTTF.json +# Contains: +# Facet_NSTTF.json # Facet design (x,y,z) corners, centroid +# +# Files resulting from a specific measurement at a specific time. +# Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A1_intensity_response\image_calibration_scaling_20250818_163358.h5 +# Contains: +# image_calibration_scaling_20250818_163358.h5 # Projector-to-camera response curve +# 20250818_163443_measurement_fringe.h5 # Captured fringe images, plus distance-to-mirror, etc. +# +# Directory containing measurement postprocessing results +# Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post_p001_default\ +# Contains: +# This file "20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini" +# +# +# Then navigate to the directory containing the file "example_process_single_facet,py" and execute: +# +# python example_process_single_facet.py --verbose -s Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post_p001_default\20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini +# +# +# You can modify the parameters to accomplish different purposes. For example, suppose you want a version +# that performs quick analysis that turns off the ray tracing and most of the plotting. +# To do that, perform the following: +# +# 1. Create a new postprocess id, for example "p002_fast". +# +# 2. Create a new directory for the postprocessing results +# Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post_p002_fast\ +# +# 3. Copy this file into the new directory, with a new name: +# 20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini +# Note it contains the new "p002_fast" postprocess id. +# +# 4. Edit the new file: +# post_process_id = p002_fast +# plots.options_slope_deviation_vis.to_plot = False +# plots.options_curvature_vis.to_plot = False +# plots.options_ray_trace_vis.to_plot = False +# The other settings for the slope deviation, curvature, and ray tracing can be +# either deleted or left in place, since they will be ignored. +# +# 5. Modify the plot control parameters for the slope plots as desired. +# +# 6. Execute, pointing to the new .ini file: +# +# python example_process_single_facet.py --verbose -s Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post_p002_fast\20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini +# +# You can use a similar approach to develop post-processing commands for producing higher resolution +# slope maps, ray-tracing from differ light source positions, differnt reay-tracing resolution, etc. +# + + +[Default] +# Whether to output detailed progress output, intermediate calculation results, etc. +verbose = True + +# Directory Setup +file_camera = Q:\Instruments\OLSL_OpticsLabSofastLandscape\camera_sofast_optics_lab_landscape_2025_02.h5 +file_orientation = Q:\Instruments\OLSL_OpticsLabSofastLandscape\spatial_orientation_optics_lab_landscape.h5 +file_display = Q:\Instruments\OLSL_OpticsLabSofastLandscape\OLSLrsqw_OpticsLabSofastLandscapeFringeSquareWhite\display_shape_optics_lab_landscape_square_distorted_3d_100x100.h5 +file_facet = Q:\Mirrors\SNLTF\0_Common\Facet_NSTTF.json +file_calibration = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A1_intensity_response\image_calibration_scaling_20250818_163358.h5 +file_measurement = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A2_collection_result\20250818_163443_measurement_fringe.h5 + +# Directory to write output files. +dir_save = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post_p001_default + +# Strings to denote this computation. Must be contiguous, with no space, tabs or line feeds. +measurement_id = 20250818_163443_SLTF-A_OLSLrsqw +post_process_id = p001_default + +# Plot control parameters +# For detaled definitions and default values, see StandardPlotOutput.py, particularly the classes +# _OptionsFileOutput, _OptionsSlopeVis, _OptionsSlopeDeviationVis, _OptionsCurvatureVis, _OptionsRayTraceVis, and _RayTraceParameters. +# +# Plot control flags are optional and may be omitted from this file if brevity is desired. +# ------------------------------------------------------------------------------------------- +# File output control +plots.options_file_output.to_save = True +plots.options_file_output.save_dpi = 200 +plots.options_file_output.save_format = png +plots.options_file_output.close_after_save = False +plots.options_file_output.number_in_name = False + +# Slope plot control +plots.options_slope_vis.resolution = 0.001 +plots.options_slope_vis.clim = 7 +plots.options_slope_vis.quiver_density = 0.1 +plots.options_slope_vis.quiver_scale = 25 +plots.options_slope_vis.quiver_color = white +plots.options_slope_vis.to_plot = True + +# Slope deviation plot control +plots.options_slope_deviation_vis.resolution = 0.001 +plots.options_slope_deviation_vis.clim = 1.5 +plots.options_slope_deviation_vis.quiver_density = 0.1 +plots.options_slope_deviation_vis.quiver_scale = 25 +plots.options_slope_deviation_vis.quiver_color = white +plots.options_slope_deviation_vis.to_plot = True + +# Curvature plot control +plots.options_curvature_vis.resolution = 0.001 +plots.options_curvature_vis.clim = 50 +plots.options_curvature_vis.processing_1 = None +plots.options_curvature_vis.processing_2 = None +plots.options_curvature_vis.smooth_kernel_width = 1 +plots.options_curvature_vis.to_plot = True + +# Ray trace plot control +plots.options_ray_trace_vis.ray_trace_optic_res = 0.05 +plots.options_ray_trace_vis.hist_bin_res = 0.07 +plots.options_ray_trace_vis.hist_extent = 3 +plots.options_ray_trace_vis.enclosed_energy_max_semi_width = 1 +plots.options_ray_trace_vis.to_plot = True + +# Ray trace parameters +plots.options_ray_trace_vis.sun_direction_x = 0 +plots.options_ray_trace_vis.sun_direction_y = 0 +plots.options_ray_trace_vis.sun_direction_z = -1 +plots.options_ray_trace_vis.sun_sample_resolution = 40 + +plots.options_ray_trace_vis.v_target_center_x = 0 +plots.options_ray_trace_vis.v_target_center_y = 0 +plots.options_ray_trace_vis.v_target_center_z = 100 + +plots.options_ray_trace_vis.v_target_normal_x = 0 +plots.options_ray_trace_vis.v_target_normal_y = 0 +plots.options_ray_trace_vis.v_target_normal_z = -1 diff --git a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini new file mode 100644 index 000000000..85c37aafb --- /dev/null +++ b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini @@ -0,0 +1,161 @@ +# This file provides the data paths and control parameters for the example_process_single_facet.py example. +# +# To use this file, copy it to a new filename and update the values below to point at your data source. +# Original file: +# /example/sofast_fringe/example_process_single_facet_Q.ini +# +# To run the standard example using the values within this file, create a directory heirarchy: +# +# Directory containing mirror measurement results +# Q:\Mirrors\ +# +# Results for Sandia Tower Heliostat Facets +# Q:\Mirrors\SNLTF\ +# +# Results for Sandia Tower Heliostat Facet "A" +# Q:\Mirrors\SNLTF\SNLTF-A\ +# +# Measurement of facet "A" with the Optics Lab SOFAST Landscape setup, measuring in fringe mode, square image projection, white screen +# Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\ +# +# Directory containing SOFAST calibration files, constant and applicable to all measurement modes. +# Q:\Instruments\OLSL_OpticsLabSofastLandscape\ +# Contains: +# camera_sofast_optics_lab_landscape_2025_02.h5 # Camera lens calibration +# spatial_orientation_optics_lab_landscape.h5 # Relative positions of camera and screen +# +# Directory containing SOFAST calibration files, constant and used by the fringe measurement mode, square projection, white screen. +# Q:\Instruments\OLSL_OpticsLabSofastLandscape\OLSLrsqw_OpticsLabSofastLandscapeFringeSquareWhite\ +# Contains: +# display_shape_optics_lab_landscape_square_distorted_3d_100x100.h5 # Screen 3-d shape (waves, ripples,...) +# +# Directory containing files defining the mirror to measure. +# Q:\Mirrors\SNLTF\0_Common\Facet_NSTTF.json +# Contains: +# Facet_NSTTF.json # Facet design (x,y,z) corners, centroid +# +# Files resulting from a specific measurement at a specific time. +# Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A1_intensity_response\image_calibration_scaling_20250818_163358.h5 +# Contains: +# image_calibration_scaling_20250818_163358.h5 # Projector-to-camera response curve +# 20250818_163443_measurement_fringe.h5 # Captured fringe images, plus distance-to-mirror, etc. +# +# Directory containing measurement postprocessing results +# Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post_p001_default\ +# Contains: +# This file "20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini" +# +# +# Then navigate to the directory containing the file "example_process_single_facet,py" and execute: +# +# python example_process_single_facet.py --verbose -s Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post_p001_default\20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini +# +# +# You can modify the parameters to accomplish different purposes. For example, suppose you want a version +# that performs quick analysis that turns off the ray tracing and most of the plotting. +# To do that, perform the following: +# +# 1. Create a new postprocess id, for example "p002_fast". +# +# 2. Create a new directory for the postprocessing results +# Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post_p002_fast\ +# +# 3. Copy this file into the new directory, with a new name: +# 20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini +# Note it contains the new "p002_fast" postprocess id. +# +# 4. Edit the new file: +# dir_save = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post_p002_fast +# post_process_id = p002_fast +# plots.options_slope_deviation_vis.to_plot = False +# plots.options_curvature_vis.to_plot = False +# plots.options_ray_trace_vis.to_plot = False +# The other settings for the slope deviation, curvature, and ray tracing can be +# either deleted or left in place, since they will be ignored. +# +# 5. Modify the plot control parameters for the slope plots as desired. +# +# 6. Execute, pointing to the new .ini file: +# +# python example_process_single_facet.py --verbose -s Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post_p002_fast\20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini +# +# You can use a similar approach to develop post-processing commands for producing higher resolution +# slope maps, ray-tracing from differ light source positions, differnt reay-tracing resolution, etc. +# + +[Default] +# Whether to output detailed progress output, intermediate calculation results, etc. +verbose = True + +# Directory Setup +file_camera = Q:\Instruments\OLSL_OpticsLabSofastLandscape\camera_sofast_optics_lab_landscape_2025_02.h5 +file_orientation = Q:\Instruments\OLSL_OpticsLabSofastLandscape\spatial_orientation_optics_lab_landscape.h5 +file_display = Q:\Instruments\OLSL_OpticsLabSofastLandscape\OLSLrsqw_OpticsLabSofastLandscapeFringeSquareWhite\display_shape_optics_lab_landscape_square_distorted_3d_100x100.h5 +file_facet = Q:\Mirrors\SNLTF\0_Common\Facet_NSTTF.json +file_calibration = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A1_intensity_response\image_calibration_scaling_20250818_163358.h5 +file_measurement = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A2_collection_result\20250818_163443_measurement_fringe.h5 + +# Directory to write output files. +dir_save = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post_p002_fast + +# Strings to denote this computation. Must be contiguous, with no space, tabs or line feeds. +measurement_id = 20250818_163443_SLTF-A_OLSLrsqw +post_process_id = p002_fast + +# Plot control parameters +# For detaled definitions and default values, see StandardPlotOutput.py, particularly the classes +# _OptionsFileOutput, _OptionsSlopeVis, _OptionsSlopeDeviationVis, _OptionsCurvatureVis, _OptionsRayTraceVis, and _RayTraceParameters. +# +# Plot control flags are optional and may be omitted from this file if brevity is desired. +# ------------------------------------------------------------------------------------------- +# File output control +plots.options_file_output.to_save = True +plots.options_file_output.save_dpi = 200 +plots.options_file_output.save_format = png +plots.options_file_output.close_after_save = False +plots.options_file_output.number_in_name = False + +# Slope plot control +plots.options_slope_vis.resolution = 0.001 +plots.options_slope_vis.clim = 7 +plots.options_slope_vis.quiver_density = 0.1 +plots.options_slope_vis.quiver_scale = 25 +plots.options_slope_vis.quiver_color = white +plots.options_slope_vis.to_plot = True + +# Slope deviation plot control +plots.options_slope_deviation_vis.resolution = 0.001 +plots.options_slope_deviation_vis.clim = 1.5 +plots.options_slope_deviation_vis.quiver_density = 0.1 +plots.options_slope_deviation_vis.quiver_scale = 25 +plots.options_slope_deviation_vis.quiver_color = white +plots.options_slope_deviation_vis.to_plot = False + +# Curvature plot control +plots.options_curvature_vis.resolution = 0.001 +plots.options_curvature_vis.clim = 50 +plots.options_curvature_vis.processing_1 = None +plots.options_curvature_vis.processing_2 = None +plots.options_curvature_vis.smooth_kernel_width = 1 +plots.options_curvature_vis.to_plot = False + +# Ray trace plot control +plots.options_ray_trace_vis.ray_trace_optic_res = 0.05 +plots.options_ray_trace_vis.hist_bin_res = 0.07 +plots.options_ray_trace_vis.hist_extent = 3 +plots.options_ray_trace_vis.enclosed_energy_max_semi_width = 1 +plots.options_ray_trace_vis.to_plot = False + +# Ray trace parameters +plots.options_ray_trace_vis.sun_direction_x = 0 +plots.options_ray_trace_vis.sun_direction_y = 0 +plots.options_ray_trace_vis.sun_direction_z = -1 +plots.options_ray_trace_vis.sun_sample_resolution = 40 + +plots.options_ray_trace_vis.v_target_center_x = 0 +plots.options_ray_trace_vis.v_target_center_y = 0 +plots.options_ray_trace_vis.v_target_center_z = 100 + +plots.options_ray_trace_vis.v_target_normal_x = 0 +plots.options_ray_trace_vis.v_target_normal_y = 0 +plots.options_ray_trace_vis.v_target_normal_z = -1 diff --git a/example/sofast_fringe/example_process_single_facet.py b/example/sofast_fringe/single_facet/example_process_single_facet.py similarity index 97% rename from example/sofast_fringe/example_process_single_facet.py rename to example/sofast_fringe/single_facet/example_process_single_facet.py index d9b5140a4..83ea0ff06 100644 --- a/example/sofast_fringe/example_process_single_facet.py +++ b/example/sofast_fringe/single_facet/example_process_single_facet.py @@ -212,7 +212,7 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v file_calibration = join(dir_data_sofast, "data_measurement/image_calibration.h5") file_measurement = join(dir_data_sofast, "data_measurement/measurement_facet.h5") # Define save dir - dir_save = join(dirname(__file__), "data/output/single_facet") + dir_save = join(dirname(__file__), "../data/output/single_facet") # Strings denoting computation. measurement_id = "Time_Mirror_InstrumentMode" post_process_id = "PostSpec" diff --git a/example/sofast_fringe/example_process_single_facet_README.md b/example/sofast_fringe/single_facet/example_process_single_facet_README.md similarity index 100% rename from example/sofast_fringe/example_process_single_facet_README.md rename to example/sofast_fringe/single_facet/example_process_single_facet_README.md diff --git a/opencsp/common/lib/csp/StandardPlotOutput.py b/opencsp/common/lib/csp/StandardPlotOutput.py index 519c9f2ad..3ab12370b 100644 --- a/opencsp/common/lib/csp/StandardPlotOutput.py +++ b/opencsp/common/lib/csp/StandardPlotOutput.py @@ -20,6 +20,24 @@ import opencsp.common.lib.tool.string_tools as st +@dataclass +class _OptionsFileOutput: + to_save: bool = False + """Flag to save figures or not. (default False)""" + output_dir: str = '' + """Output path to save directory. (default '')""" + save_dpi: int = 200 + """Dots Per Inch (DPI) of saved figures. (default 200)""" + save_format: str = 'png' + """Saved figure format. (default 'png')""" + close_after_save: bool = False + """To close figures after save. (default False)""" + number_in_name: bool = True + """To keep figure number in save name. (default True)""" + file_prefix: str = None + """String to prefix each output file, including separator""" + + @dataclass class _OptionsSlopeVis: resolution: float = 0.01 @@ -77,7 +95,7 @@ class _OptionsCurvatureVis: Can be single value of tuple of three values to map to [x, y, combined] plots individually. (default 50)""" processing: list[str] | tuple[list[str], list[str], list[str]] = field(default_factory=list) """Processing string to apply when in MirrorAbstract.plot_orthorectified_curvature(). - Can be single value of tuple of three values to map to [x, y, combined] plots individually. + Can be single value or tuple of three values to map to [x, y, combined] plots individually. (default [])""" smooth_kernel_width: float | tuple[float, float, float] = 1 """Width of square smoothing kernel (pixels) to apply to curvature images in MirrorAbstract.plot_orthorectified_curvature(). @@ -101,24 +119,6 @@ class _OptionsRayTraceVis: """Flag to produce plots or not. (default True)""" -@dataclass -class _OptionsFileOutput: - to_save: bool = False - """Flag to save figures or not. (default False)""" - output_dir: str = '' - """Output path to save directory. (default '')""" - save_dpi: int = 200 - """Dots Per Inch (DPI) of saved figures. (default 200)""" - save_format: str = 'png' - """Saved figure format. (default 'png')""" - close_after_save: bool = False - """To close figures after save. (default False)""" - number_in_name: bool = True - """To keep figure number in save name. (default True)""" - file_prefix: str = None - """String to prefix each output file, including separator""" - - @dataclass class _RayTraceParameters: source = LightSourceSun.from_given_sun_position(Uxyz((0, 0, -1)), resolution=20) @@ -193,34 +193,136 @@ def set_plot_control_from_settings(self, settings: configparser.ConfigParser): """ # File output control. + # This should be kept in synch with class _OptionsFileOutput above. if ("Default" in settings) and ("plots.options_file_output.to_save" in settings["Default"]): self.options_file_output.to_save = st.convert_true_false_string_to_boolean( settings["Default"]["plots.options_file_output.to_save"] ) + # We expect output_dir to be handled by other routines. + if ("Default" in settings) and ("plots.options_file_output.save_dpi" in settings["Default"]): + self.options_file_output.save_dpi = int(settings["Default"]["plots.options_file_output.save_dpi"]) + if ("Default" in settings) and ("plots.options_file_output.save_format" in settings["Default"]): + self.options_file_output.save_format = str(settings["Default"]["plots.options_file_output.save_format"]) + if ("Default" in settings) and ("plots.options_file_output.close_after_save" in settings["Default"]): + self.options_file_output.close_after_save = st.convert_true_false_string_to_boolean( + settings["Default"]["plots.options_file_output.close_after_save"] + ) if ("Default" in settings) and ("plots.options_file_output.number_in_name" in settings["Default"]): self.options_file_output.number_in_name = st.convert_true_false_string_to_boolean( settings["Default"]["plots.options_file_output.number_in_name"] ) + # We expect file_prefix to be handled by other routines. # Slope plot control. - if ("Default" in settings) and ("plots.options_slope_vis.clim" in settings["Default"]): - self.options_slope_vis.clim = float(settings["Default"]["plots.options_slope_vis.clim"]) + # This should be kept in synch with class _OptionsSlopeVis above. if ("Default" in settings) and ("plots.options_slope_vis.resolution" in settings["Default"]): self.options_slope_vis.resolution = float(settings["Default"]["plots.options_slope_vis.resolution"]) + if ("Default" in settings) and ("plots.options_slope_vis.clim" in settings["Default"]): + self.options_slope_vis.clim = float(settings["Default"]["plots.options_slope_vis.clim"]) + if ("Default" in settings) and ("plots.options_slope_vis.quiver_density" in settings["Default"]): + self.options_slope_vis.quiver_density = float(settings["Default"]["plots.options_slope_vis.quiver_density"]) + if ("Default" in settings) and ("plots.options_slope_vis.quiver_scale" in settings["Default"]): + self.options_slope_vis.quiver_scale = float(settings["Default"]["plots.options_slope_vis.quiver_scale"]) + if ("Default" in settings) and ("plots.options_slope_vis.quiver_color" in settings["Default"]): + self.options_slope_vis.quiver_color = str(settings["Default"]["plots.options_slope_vis.quiver_color"]) + if ("Default" in settings) and ("plots.options_slope_vis.to_plot" in settings["Default"]): + self.options_slope_vis.to_plot = st.convert_true_false_string_to_boolean( + settings["Default"]["plots.options_slope_vis.to_plot"] + ) # Slope deviation plot control. - if ("Default" in settings) and ("plots.options_slope_deviation_vis.clim" in settings["Default"]): - self.options_slope_deviation_vis.clim = float(settings["Default"]["plots.options_slope_deviation_vis.clim"]) + # This should be kept in synch with class _OptionsSlopeDeviationVis above. if ("Default" in settings) and ("plots.options_slope_deviation_vis.resolution" in settings["Default"]): self.options_slope_deviation_vis.resolution = float( settings["Default"]["plots.options_slope_deviation_vis.resolution"] ) + if ("Default" in settings) and ("plots.options_slope_deviation_vis.clim" in settings["Default"]): + self.options_slope_deviation_vis.clim = float(settings["Default"]["plots.options_slope_deviation_vis.clim"]) + if ("Default" in settings) and ("plots.options_slope_deviation_vis.quiver_density" in settings["Default"]): + self.options_slope_deviation_vis.quiver_density = float( + settings["Default"]["plots.options_slope_deviation_vis.quiver_density"] + ) + if ("Default" in settings) and ("plots.options_slope_deviation_vis.quiver_scale" in settings["Default"]): + self.options_slope_deviation_vis.quiver_scale = float( + settings["Default"]["plots.options_slope_deviation_vis.quiver_scale"] + ) + if ("Default" in settings) and ("plots.options_slope_deviation_vis.quiver_color" in settings["Default"]): + self.options_slope_deviation_vis.quiver_color = settings["Default"][ + "plots.options_slope_deviation_vis.quiver_color" + ] + if ("Default" in settings) and ("plots.options_slope_deviation_vis.to_plot" in settings["Default"]): + self.options_slope_deviation_vis.to_plot = st.convert_true_false_string_to_boolean( + settings["Default"]["plots.options_slope_deviation_vis.to_plot"] + ) # Curvature plot control. + # This should be kept in synch with class _OptionsCurvatureVis above. if ("Default" in settings) and ("plots.options_curvature_vis.resolution" in settings["Default"]): self.options_curvature_vis.resolution = float(settings["Default"]["plots.options_curvature_vis.resolution"]) + if ("Default" in settings) and ("plots.options_curvature_vis.clim" in settings["Default"]): + self.options_curvature_vis.clim = float(settings["Default"]["plots.options_curvature_vis.clim"]) + processing = [] + processing_options = [ + "log", + "smooth", + ] # See VisualizeOrthorectifiedSlopeAbstract.py, routine plot_orthorectified_curvature(). + if ("Default" in settings) and ("plots.options_curvature_vis.processing_1" in settings["Default"]): + processing_1 = settings["Default"]["plots.options_curvature_vis.processing_1"] + if processing_1 in processing_options: + processing.append(processing_1) + elif processing_1 == "None": + pass + else: + lt.error_and_raise( + ValueError, f'Curvature procesing option "{processing_1}" is not one of {processing_options}.' + ) + if ("Default" in settings) and ("plots.options_curvature_vis.processing_2" in settings["Default"]): + processing_2 = settings["Default"]["plots.options_curvature_vis.processing_2"] + if processing_2 in processing_options: + processing.append(processing_2) + elif processing_2 == "None": + pass + else: + lt.error_and_raise( + ValueError, f'Curvature procesing option "{processing_2}" is not one of {processing_options}.' + ) + self.options_curvature_vis.processing = processing + if ("Default" in settings) and ("plots.options_curvature_vis.smooth_kernel_width" in settings["Default"]): + self.options_curvature_vis.smooth_kernel_width = float( + settings["Default"]["plots.options_curvature_vis.smooth_kernel_width"] + ) + if ("Default" in settings) and ("plots.options_curvature_vis.to_plot" in settings["Default"]): + self.options_curvature_vis.to_plot = st.convert_true_false_string_to_boolean( + settings["Default"]["plots.options_curvature_vis.to_plot"] + ) # Ray trace control. + # This should be kept in synch with class _OptionsRayTraceVis above. + if ("Default" in settings) and ("plots.options_ray_trace_vis.ray_trace_optic_res" in settings["Default"]): + self.options_ray_trace_vis.ray_trace_optic_res = float( + settings["Default"]["plots.options_ray_trace_vis.ray_trace_optic_res"] + ) + if ("Default" in settings) and ("plots.options_ray_trace_vis.hist_bin_res" in settings["Default"]): + self.options_ray_trace_vis.hist_bin_res = float( + settings["Default"]["plots.options_ray_trace_vis.hist_bin_res"] + ) + if ("Default" in settings) and ("plots.options_ray_trace_vis.hist_extent" in settings["Default"]): + self.options_ray_trace_vis.hist_extent = float( + settings["Default"]["plots.options_ray_trace_vis.hist_extent"] + ) + if ("Default" in settings) and ( + "plots.options_ray_trace_vis.enclosed_energy_max_semi_width" in settings["Default"] + ): + self.options_ray_trace_vis.enclosed_energy_max_semi_width = float( + settings["Default"]["plots.options_ray_trace_vis.enclosed_energy_max_semi_width"] + ) + if ("Default" in settings) and ("plots.options_ray_trace_vis.to_plot" in settings["Default"]): + self.options_ray_trace_vis.to_plot = st.convert_true_false_string_to_boolean( + settings["Default"]["plots.options_ray_trace_vis.to_plot"] + ) + + # Ray trace parameters. + # This should be kept in synch with class _RayTraceParameters above. # Light source. if ( ("Default" in settings) @@ -259,13 +361,6 @@ def set_plot_control_from_settings(self, settings: configparser.ConfigParser): v_target_normal_y = float(settings["Default"]["plots.options_ray_trace_vis.v_target_normal_y"]) v_target_normal_z = float(settings["Default"]["plots.options_ray_trace_vis.v_target_normal_z"]) self.params_ray_trace.v_target_normal = Vxyz((v_target_normal_x, v_target_normal_y, v_target_normal_z)) - # Enclosed energy plot semi-width. - if ("Default" in settings) and ( - "plots.options_ray_trace_vis.enclosed_energy_max_semi_width" in settings["Default"] - ): - self.options_ray_trace_vis.enclosed_energy_max_semi_width = float( - settings["Default"]["plots.options_ray_trace_vis.enclosed_energy_max_semi_width"] - ) def plot(self): """Creates standard output plot suite""" @@ -338,7 +433,9 @@ def _process_plot_options(self, value) -> list: elif len(value) in [0, 1]: return [value] * 3 else: - lt.error_and_raise(ValueError, f'Plot option must be length 3 or 1, not length {len(value):d}') + lt.error_and_raise( + ValueError, f'Plot option "{value}" must be length 3 or 1, not length {len(value):d}' + ) else: return [value] * 3 From 429d2dc73e8cfcf147f11738dc0f848169c29cb4 Mon Sep 17 00:00:00 2001 From: rcbrost Date: Thu, 22 Jan 2026 15:20:00 -0700 Subject: [PATCH 25/29] For example_process_single_facet.py, added process models for fast, ref 25 m, ref plano cases, extended control to allow switching off fringe image output. --- ...efault_process_single_facet_settings_Q.ini | 73 +++++++- ...2_fast_process_single_facet_settings_Q.ini | 119 ++++-------- ...ef_25m_process_single_facet_settings_Q.ini | 108 +++++++++++ ..._plano_process_single_facet_settings_Q.ini | 108 +++++++++++ .../example_process_single_facet.py | 177 +++++++++++++----- opencsp/common/lib/csp/MirrorParametric.py | 4 + 6 files changed, 451 insertions(+), 138 deletions(-) create mode 100644 example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p003_ref_25m_process_single_facet_settings_Q.ini create mode 100644 example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p004_ref_plano_process_single_facet_settings_Q.ini diff --git a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini index de262fcc8..459341d5e 100644 --- a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini +++ b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini @@ -1,5 +1,12 @@ # This file provides the data paths and control parameters for the example_process_single_facet.py example. # +# This file demonstrates a scenario where a systematic file and directory convention is used to track +# multiple measurements of multiple mirrors, in a facility with multiple optical metrology setups, +# and under multiple settings of analysis parameters and output control. +# +# This file is designed for users to emulate by copying the file and setting their own data paths +# and execution preferences. +# # To use this file, copy it to a new filename and update the values below to point at your data source. # Original file: # /example/sofast_fringe/example_process_single_facet_Q.ini @@ -35,10 +42,32 @@ # Facet_NSTTF.json # Facet design (x,y,z) corners, centroid # # Files resulting from a specific measurement at a specific time. -# Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A1_intensity_response\image_calibration_scaling_20250818_163358.h5 +# Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\ +# Contains: +# A_Collect\ # Data collection results +# B_Post_p001_default\ # Post-processing results, assumption set #1 +# 20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini +# Control file for producing B_Post_p001_default\ +# Note that "p001_default" is the post_process_id linking the control file and results. +# ... +# +# +# Files resulting from a specific measurement at a specific time. +# Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\ +# Contains: +# A1_intensity_response\ # Results of light-response check +# A2_collection_result\ # Results of fringe data capture +# +# Files resulting from a specific measurement at a specific time. +# Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A1_intensity_response\ # Contains: # image_calibration_scaling_20250818_163358.h5 # Projector-to-camera response curve -# 20250818_163443_measurement_fringe.h5 # Captured fringe images, plus distance-to-mirror, etc. +# +# Files resulting from a specific measurement at a specific time. +# Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A2_collection_result\ +# Contains: +# 20250818_163443_measurement_fringe.h5 # Captured fringe images, plus distance-to-mirror, etc. +# 20250818_163443_slope_magnitude_fringe_xy.png # (optional) Slope magnitude plot quickly generated at data capture time. # # Directory containing measurement postprocessing results # Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post_p001_default\ @@ -79,7 +108,11 @@ # python example_process_single_facet.py --verbose -s Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post_p002_fast\20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini # # You can use a similar approach to develop post-processing commands for producing higher resolution -# slope maps, ray-tracing from differ light source positions, differnt reay-tracing resolution, etc. +# slope maps, ray-tracing from different light source positions, different ray-tracing resolution, etc. +# +# You can also use this approach to execute post-processing under different assumptions, such as +# reference mirror focal length. This is accomplished using similar steps, but setting an appropriate +# post_process_id and modifying the fields that control the analysis aspect you desire to modify. # @@ -87,7 +120,11 @@ # Whether to output detailed progress output, intermediate calculation results, etc. verbose = True -# Directory Setup +# Strings to denote this computation. Must be contiguous, with no space, tabs or line feeds. +measurement_id = 20250818_163443_SLTF-A_OLSLrsqw +post_process_id = p001_default + +# Input file_camera = Q:\Instruments\OLSL_OpticsLabSofastLandscape\camera_sofast_optics_lab_landscape_2025_02.h5 file_orientation = Q:\Instruments\OLSL_OpticsLabSofastLandscape\spatial_orientation_optics_lab_landscape.h5 file_display = Q:\Instruments\OLSL_OpticsLabSofastLandscape\OLSLrsqw_OpticsLabSofastLandscapeFringeSquareWhite\display_shape_optics_lab_landscape_square_distorted_3d_100x100.h5 @@ -96,18 +133,34 @@ file_calibration = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A file_measurement = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A2_collection_result\20250818_163443_measurement_fringe.h5 # Directory to write output files. -dir_save = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post_p001_default +# (The post_process_id below will be added as a suffix.) +dir_save_root = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post -# Strings to denote this computation. Must be contiguous, with no space, tabs or line feeds. -measurement_id = 20250818_163443_SLTF-A_OLSLrsqw -post_process_id = p001_default +# Analysis control parameters +# Distance values are meters. +fit_initial_focal_length_x = 300.0 +fit_initial_focal_length_y = 300.0 +fit_robust_least_squares = True +fit_downsample = 10 + +# Reference mirror +# As of this writing, surface choices are "plano" and "symmetric_paraboloid". +# Distance values are meters. +#reference_mirror_surface = plano +reference_mirror_surface_type = symmetric_paraboloid +reference_mirror_focal_length = 100 + +# Output control +# Output control flags are optional and may be omitted if brevity is desired. +# --------------------------------------------------------------------------- +output_fringe_images = True # Plot control parameters # For detaled definitions and default values, see StandardPlotOutput.py, particularly the classes # _OptionsFileOutput, _OptionsSlopeVis, _OptionsSlopeDeviationVis, _OptionsCurvatureVis, _OptionsRayTraceVis, and _RayTraceParameters. # -# Plot control flags are optional and may be omitted from this file if brevity is desired. -# ------------------------------------------------------------------------------------------- +# Plot control flags are optional and may be omitted if brevity is desired. +# ------------------------------------------------------------------------- # File output control plots.options_file_output.to_save = True plots.options_file_output.save_dpi = 200 diff --git a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini index 85c37aafb..aa75729b1 100644 --- a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini +++ b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini @@ -1,93 +1,24 @@ # This file provides the data paths and control parameters for the example_process_single_facet.py example. # -# To use this file, copy it to a new filename and update the values below to point at your data source. -# Original file: -# /example/sofast_fringe/example_process_single_facet_Q.ini +# This file demonstrates a scenario where a systematic file and directory convention is used to track +# multiple measurements of multiple mirrors, in a facility with multiple optical metrology setups, +# and under multiple settings of analysis parameters and output control. # -# To run the standard example using the values within this file, create a directory heirarchy: +# For detailed instructions on the context, see the default processing file: +# 20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini # -# Directory containing mirror measurement results -# Q:\Mirrors\ -# -# Results for Sandia Tower Heliostat Facets -# Q:\Mirrors\SNLTF\ -# -# Results for Sandia Tower Heliostat Facet "A" -# Q:\Mirrors\SNLTF\SNLTF-A\ -# -# Measurement of facet "A" with the Optics Lab SOFAST Landscape setup, measuring in fringe mode, square image projection, white screen -# Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\ -# -# Directory containing SOFAST calibration files, constant and applicable to all measurement modes. -# Q:\Instruments\OLSL_OpticsLabSofastLandscape\ -# Contains: -# camera_sofast_optics_lab_landscape_2025_02.h5 # Camera lens calibration -# spatial_orientation_optics_lab_landscape.h5 # Relative positions of camera and screen -# -# Directory containing SOFAST calibration files, constant and used by the fringe measurement mode, square projection, white screen. -# Q:\Instruments\OLSL_OpticsLabSofastLandscape\OLSLrsqw_OpticsLabSofastLandscapeFringeSquareWhite\ -# Contains: -# display_shape_optics_lab_landscape_square_distorted_3d_100x100.h5 # Screen 3-d shape (waves, ripples,...) -# -# Directory containing files defining the mirror to measure. -# Q:\Mirrors\SNLTF\0_Common\Facet_NSTTF.json -# Contains: -# Facet_NSTTF.json # Facet design (x,y,z) corners, centroid -# -# Files resulting from a specific measurement at a specific time. -# Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A1_intensity_response\image_calibration_scaling_20250818_163358.h5 -# Contains: -# image_calibration_scaling_20250818_163358.h5 # Projector-to-camera response curve -# 20250818_163443_measurement_fringe.h5 # Captured fringe images, plus distance-to-mirror, etc. -# -# Directory containing measurement postprocessing results -# Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post_p001_default\ -# Contains: -# This file "20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini" -# -# -# Then navigate to the directory containing the file "example_process_single_facet,py" and execute: -# -# python example_process_single_facet.py --verbose -s Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post_p001_default\20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini -# -# -# You can modify the parameters to accomplish different purposes. For example, suppose you want a version -# that performs quick analysis that turns off the ray tracing and most of the plotting. -# To do that, perform the following: -# -# 1. Create a new postprocess id, for example "p002_fast". -# -# 2. Create a new directory for the postprocessing results -# Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post_p002_fast\ -# -# 3. Copy this file into the new directory, with a new name: -# 20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini -# Note it contains the new "p002_fast" postprocess id. -# -# 4. Edit the new file: -# dir_save = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post_p002_fast -# post_process_id = p002_fast -# plots.options_slope_deviation_vis.to_plot = False -# plots.options_curvature_vis.to_plot = False -# plots.options_ray_trace_vis.to_plot = False -# The other settings for the slope deviation, curvature, and ray tracing can be -# either deleted or left in place, since they will be ignored. -# -# 5. Modify the plot control parameters for the slope plots as desired. -# -# 6. Execute, pointing to the new .ini file: -# -# python example_process_single_facet.py --verbose -s Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post_p002_fast\20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini -# -# You can use a similar approach to develop post-processing commands for producing higher resolution -# slope maps, ray-tracing from differ light source positions, differnt reay-tracing resolution, etc. +# This version is modified to run more quickly by skipping ray tracing and some of the plots. # [Default] # Whether to output detailed progress output, intermediate calculation results, etc. verbose = True -# Directory Setup +# Strings to denote this computation. Must be contiguous, with no space, tabs or line feeds. +measurement_id = 20250818_163443_SLTF-A_OLSLrsqw +post_process_id = p002_fast + +# Input file_camera = Q:\Instruments\OLSL_OpticsLabSofastLandscape\camera_sofast_optics_lab_landscape_2025_02.h5 file_orientation = Q:\Instruments\OLSL_OpticsLabSofastLandscape\spatial_orientation_optics_lab_landscape.h5 file_display = Q:\Instruments\OLSL_OpticsLabSofastLandscape\OLSLrsqw_OpticsLabSofastLandscapeFringeSquareWhite\display_shape_optics_lab_landscape_square_distorted_3d_100x100.h5 @@ -96,18 +27,34 @@ file_calibration = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A file_measurement = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A2_collection_result\20250818_163443_measurement_fringe.h5 # Directory to write output files. -dir_save = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post_p002_fast +# (The post_process_id below will be added as a suffix.) +dir_save_root = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post -# Strings to denote this computation. Must be contiguous, with no space, tabs or line feeds. -measurement_id = 20250818_163443_SLTF-A_OLSLrsqw -post_process_id = p002_fast +# Analysis control parameters +# Distance values are meters. +fit_initial_focal_length_x = 300.0 +fit_initial_focal_length_y = 300.0 +fit_robust_least_squares = True +fit_downsample = 10 + +# Reference mirror +# As of this writing, surface choices are "plano" and "symmetric_paraboloid". +# Distance values are meters. +#reference_mirror_surface = plano +reference_mirror_surface_type = symmetric_paraboloid +reference_mirror_focal_length = 100 + +# Output control +# Output control flags are optional and may be omitted if brevity is desired. +# --------------------------------------------------------------------------- +output_fringe_images = False # Plot control parameters # For detaled definitions and default values, see StandardPlotOutput.py, particularly the classes # _OptionsFileOutput, _OptionsSlopeVis, _OptionsSlopeDeviationVis, _OptionsCurvatureVis, _OptionsRayTraceVis, and _RayTraceParameters. # -# Plot control flags are optional and may be omitted from this file if brevity is desired. -# ------------------------------------------------------------------------------------------- +# Plot control flags are optional and may be omitted if brevity is desired. +# ------------------------------------------------------------------------- # File output control plots.options_file_output.to_save = True plots.options_file_output.save_dpi = 200 diff --git a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p003_ref_25m_process_single_facet_settings_Q.ini b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p003_ref_25m_process_single_facet_settings_Q.ini new file mode 100644 index 000000000..d70dd7c04 --- /dev/null +++ b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p003_ref_25m_process_single_facet_settings_Q.ini @@ -0,0 +1,108 @@ +# This file provides the data paths and control parameters for the example_process_single_facet.py example. +# +# This file demonstrates a scenario where a systematic file and directory convention is used to track +# multiple measurements of multiple mirrors, in a facility with multiple optical metrology setups, +# and under multiple settings of analysis parameters and output control. +# +# For detailed instructions on the context, see the default processing file: +# 20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini +# +# This version is modified to compare against a reference mirror with a 25 m focal length. +# + +[Default] +# Whether to output detailed progress output, intermediate calculation results, etc. +verbose = True + +# Strings to denote this computation. Must be contiguous, with no space, tabs or line feeds. +measurement_id = 20250818_163443_SLTF-A_OLSLrsqw +post_process_id = p003_ref_25m + +# Input +file_camera = Q:\Instruments\OLSL_OpticsLabSofastLandscape\camera_sofast_optics_lab_landscape_2025_02.h5 +file_orientation = Q:\Instruments\OLSL_OpticsLabSofastLandscape\spatial_orientation_optics_lab_landscape.h5 +file_display = Q:\Instruments\OLSL_OpticsLabSofastLandscape\OLSLrsqw_OpticsLabSofastLandscapeFringeSquareWhite\display_shape_optics_lab_landscape_square_distorted_3d_100x100.h5 +file_facet = Q:\Mirrors\SNLTF\0_Common\Facet_NSTTF.json +file_calibration = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A1_intensity_response\image_calibration_scaling_20250818_163358.h5 +file_measurement = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A2_collection_result\20250818_163443_measurement_fringe.h5 + +# Directory to write output files. +# (The post_process_id below will be added as a suffix.) +dir_save_root = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post + +# Analysis control parameters +# Distance values are meters. +fit_initial_focal_length_x = 300.0 +fit_initial_focal_length_y = 300.0 +fit_robust_least_squares = True +fit_downsample = 10 + +# Reference mirror +# As of this writing, surface choices are "plano" and "symmetric_paraboloid". +# Distance values are meters. +#reference_mirror_surface = plano +reference_mirror_surface_type = symmetric_paraboloid +reference_mirror_focal_length = 25 + +# Output control +# Output control flags are optional and may be omitted if brevity is desired. +# --------------------------------------------------------------------------- +output_fringe_images = True + +# Plot control parameters +# For detaled definitions and default values, see StandardPlotOutput.py, particularly the classes +# _OptionsFileOutput, _OptionsSlopeVis, _OptionsSlopeDeviationVis, _OptionsCurvatureVis, _OptionsRayTraceVis, and _RayTraceParameters. +# +# Plot control flags are optional and may be omitted if brevity is desired. +# ------------------------------------------------------------------------- +# File output control +plots.options_file_output.to_save = True +plots.options_file_output.save_dpi = 200 +plots.options_file_output.save_format = png +plots.options_file_output.close_after_save = False +plots.options_file_output.number_in_name = False + +# Slope plot control +plots.options_slope_vis.resolution = 0.001 +plots.options_slope_vis.clim = 7 +plots.options_slope_vis.quiver_density = 0.1 +plots.options_slope_vis.quiver_scale = 25 +plots.options_slope_vis.quiver_color = white +plots.options_slope_vis.to_plot = True + +# Slope deviation plot control +plots.options_slope_deviation_vis.resolution = 0.001 +plots.options_slope_deviation_vis.clim = 1.5 +plots.options_slope_deviation_vis.quiver_density = 0.1 +plots.options_slope_deviation_vis.quiver_scale = 25 +plots.options_slope_deviation_vis.quiver_color = white +plots.options_slope_deviation_vis.to_plot = True + +# Curvature plot control +plots.options_curvature_vis.resolution = 0.001 +plots.options_curvature_vis.clim = 50 +plots.options_curvature_vis.processing_1 = None +plots.options_curvature_vis.processing_2 = None +plots.options_curvature_vis.smooth_kernel_width = 1 +plots.options_curvature_vis.to_plot = True + +# Ray trace plot control +plots.options_ray_trace_vis.ray_trace_optic_res = 0.05 +plots.options_ray_trace_vis.hist_bin_res = 0.07 +plots.options_ray_trace_vis.hist_extent = 3 +plots.options_ray_trace_vis.enclosed_energy_max_semi_width = 1 +plots.options_ray_trace_vis.to_plot = True + +# Ray trace parameters +plots.options_ray_trace_vis.sun_direction_x = 0 +plots.options_ray_trace_vis.sun_direction_y = 0 +plots.options_ray_trace_vis.sun_direction_z = -1 +plots.options_ray_trace_vis.sun_sample_resolution = 40 + +plots.options_ray_trace_vis.v_target_center_x = 0 +plots.options_ray_trace_vis.v_target_center_y = 0 +plots.options_ray_trace_vis.v_target_center_z = 100 + +plots.options_ray_trace_vis.v_target_normal_x = 0 +plots.options_ray_trace_vis.v_target_normal_y = 0 +plots.options_ray_trace_vis.v_target_normal_z = -1 diff --git a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p004_ref_plano_process_single_facet_settings_Q.ini b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p004_ref_plano_process_single_facet_settings_Q.ini new file mode 100644 index 000000000..51ff9884b --- /dev/null +++ b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p004_ref_plano_process_single_facet_settings_Q.ini @@ -0,0 +1,108 @@ +# This file provides the data paths and control parameters for the example_process_single_facet.py example. +# +# This file demonstrates a scenario where a systematic file and directory convention is used to track +# multiple measurements of multiple mirrors, in a facility with multiple optical metrology setups, +# and under multiple settings of analysis parameters and output control. +# +# For detailed instructions on the context, see the default processing file: +# 20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini +# +# This version is modified to compare against a plano reference mirror. +# + +[Default] +# Whether to output detailed progress output, intermediate calculation results, etc. +verbose = True + +# Strings to denote this computation. Must be contiguous, with no space, tabs or line feeds. +measurement_id = 20250818_163443_SLTF-A_OLSLrsqw +post_process_id = p004_ref_plano + +# Input +file_camera = Q:\Instruments\OLSL_OpticsLabSofastLandscape\camera_sofast_optics_lab_landscape_2025_02.h5 +file_orientation = Q:\Instruments\OLSL_OpticsLabSofastLandscape\spatial_orientation_optics_lab_landscape.h5 +file_display = Q:\Instruments\OLSL_OpticsLabSofastLandscape\OLSLrsqw_OpticsLabSofastLandscapeFringeSquareWhite\display_shape_optics_lab_landscape_square_distorted_3d_100x100.h5 +file_facet = Q:\Mirrors\SNLTF\0_Common\Facet_NSTTF.json +file_calibration = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A1_intensity_response\image_calibration_scaling_20250818_163358.h5 +file_measurement = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A2_collection_result\20250818_163443_measurement_fringe.h5 + +# Directory to write output files. +# (The post_process_id below will be added as a suffix.) +dir_save_root = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post + +# Analysis control parameters +# Distance values are meters. +fit_initial_focal_length_x = 300.0 +fit_initial_focal_length_y = 300.0 +fit_robust_least_squares = True +fit_downsample = 10 + +# Reference mirror +# As of this writing, surface choices are "plano" and "symmetric_paraboloid". +# Distance values are meters. +#reference_mirror_surface = plano +reference_mirror_surface_type = plano +reference_mirror_focal_length = None + +# Output control +# Output control flags are optional and may be omitted if brevity is desired. +# --------------------------------------------------------------------------- +output_fringe_images = True + +# Plot control parameters +# For detaled definitions and default values, see StandardPlotOutput.py, particularly the classes +# _OptionsFileOutput, _OptionsSlopeVis, _OptionsSlopeDeviationVis, _OptionsCurvatureVis, _OptionsRayTraceVis, and _RayTraceParameters. +# +# Plot control flags are optional and may be omitted if brevity is desired. +# ------------------------------------------------------------------------- +# File output control +plots.options_file_output.to_save = True +plots.options_file_output.save_dpi = 200 +plots.options_file_output.save_format = png +plots.options_file_output.close_after_save = False +plots.options_file_output.number_in_name = False + +# Slope plot control +plots.options_slope_vis.resolution = 0.001 +plots.options_slope_vis.clim = 7 +plots.options_slope_vis.quiver_density = 0.1 +plots.options_slope_vis.quiver_scale = 25 +plots.options_slope_vis.quiver_color = white +plots.options_slope_vis.to_plot = True + +# Slope deviation plot control +plots.options_slope_deviation_vis.resolution = 0.001 +plots.options_slope_deviation_vis.clim = 1.5 +plots.options_slope_deviation_vis.quiver_density = 0.1 +plots.options_slope_deviation_vis.quiver_scale = 25 +plots.options_slope_deviation_vis.quiver_color = white +plots.options_slope_deviation_vis.to_plot = True + +# Curvature plot control +plots.options_curvature_vis.resolution = 0.001 +plots.options_curvature_vis.clim = 50 +plots.options_curvature_vis.processing_1 = None +plots.options_curvature_vis.processing_2 = None +plots.options_curvature_vis.smooth_kernel_width = 1 +plots.options_curvature_vis.to_plot = True + +# Ray trace plot control +plots.options_ray_trace_vis.ray_trace_optic_res = 0.05 +plots.options_ray_trace_vis.hist_bin_res = 0.07 +plots.options_ray_trace_vis.hist_extent = 3 +plots.options_ray_trace_vis.enclosed_energy_max_semi_width = 1 +plots.options_ray_trace_vis.to_plot = True + +# Ray trace parameters +plots.options_ray_trace_vis.sun_direction_x = 0 +plots.options_ray_trace_vis.sun_direction_y = 0 +plots.options_ray_trace_vis.sun_direction_z = -1 +plots.options_ray_trace_vis.sun_sample_resolution = 40 + +plots.options_ray_trace_vis.v_target_center_x = 0 +plots.options_ray_trace_vis.v_target_center_y = 0 +plots.options_ray_trace_vis.v_target_center_z = 100 + +plots.options_ray_trace_vis.v_target_normal_x = 0 +plots.options_ray_trace_vis.v_target_normal_y = 0 +plots.options_ray_trace_vis.v_target_normal_z = -1 diff --git a/example/sofast_fringe/single_facet/example_process_single_facet.py b/example/sofast_fringe/single_facet/example_process_single_facet.py index 83ea0ff06..61d21949b 100644 --- a/example/sofast_fringe/single_facet/example_process_single_facet.py +++ b/example/sofast_fringe/single_facet/example_process_single_facet.py @@ -44,9 +44,10 @@ from opencsp.app.sofast.lib.SpatialOrientation import SpatialOrientation from opencsp.common.lib.camera.Camera import Camera from opencsp.common.lib.csp.LightSourceSun import LightSourceSun -from opencsp.common.lib.csp.MirrorParametric import MirrorParametric +from opencsp.common.lib.csp.MirrorParametric import MirrorParametric, PLANO, SYMMETRIC_PARABOLOID from opencsp.common.lib.csp.StandardPlotOutput import StandardPlotOutput from opencsp.common.lib.deflectometry.Surface2DParabolic import Surface2DParabolic +from opencsp.common.lib.deflectometry.Surface2DPlano import Surface2DPlano from opencsp.common.lib.geometry.Uxyz import Uxyz from opencsp.common.lib.geometry.Vxyz import Vxyz from opencsp.common.lib.opencsp_path.opencsp_root_path import opencsp_code_dir @@ -57,15 +58,27 @@ def process_single_facet( verbose: bool, + # Input data file_camera: str, file_display: str, file_orientation: str, file_facet: str, file_calibration: str, file_measurement: str, + # Output file control dir_save: str, measurement_id: str, post_process_id: str, + # Analysis control + fit_initial_focal_length_x: float, + fit_initial_focal_length_y: float, + fit_robust_least_squares: bool, + fit_downsample: int, + # Reference mirror surface + reference_mirror_surface_type: str, + reference_mirror_focal_length: float, + # Output rendering control + output_fringe_images: bool, plots: StandardPlotOutput, ): """Performs processing of previously collected SOFAST data of single facet mirror. @@ -94,38 +107,43 @@ def process_single_facet( calibration = ImageCalibrationScaling.load_from_hdf(file_calibration) facet_data = DefinitionFacet.load_from_json(file_facet) - # 2. Save projected sinusoidal fringe images to PNG format - # ======================================================== - fringes = Fringes(measurement.fringe_periods_x, measurement.fringe_periods_y) - images = fringes.get_frames(640, 320, "uint8", [0, 255]) # writes images we projected from sofast projector to disk - dir_save_cur = join(dir_save, "B1_projected_fringes") - ft.create_directories_if_necessary(dir_save_cur) - # Save y images - for idx_image in range(measurement.num_y_ims): - image = images[..., idx_image] - imageio.imwrite(join(dir_save_cur, output_file_prefix + f"y_{idx_image:02d}.png"), image) - # Save x images - for idx_image in range(measurement.num_x_ims): - image = images[..., measurement.num_y_ims + idx_image] - imageio.imwrite(join(dir_save_cur, output_file_prefix + f"x_{idx_image:02d}.png"), image) - - # 3. Save captured sinusoidal fringe images and mask images to PNG format - # ======================================================================= - dir_save_cur = join(dir_save, "B2_captured_fringes") - ft.create_directories_if_necessary(dir_save_cur) - - # Save mask (like a pixel mask value (all 0s, all 255s)) images - for idx_image in [0, 1]: - image = measurement.mask_images[..., idx_image] - imageio.imwrite(join(dir_save_cur, output_file_prefix + f"mask_{idx_image:02d}.png"), image) - # Save y images (when lines were vertical, e.g.) - for idx_image in range(measurement.num_y_ims): - image = measurement.fringe_images_y[..., idx_image] - imageio.imwrite(join(dir_save_cur, output_file_prefix + f"y_{idx_image:02d}.png"), image) - # Save x images (when lines were horizontal, e.g.) - for idx_image in range(measurement.num_x_ims): - image = measurement.fringe_images_x[..., idx_image] - imageio.imwrite(join(dir_save_cur, output_file_prefix + f"x_{idx_image:02d}.png"), image) + if not output_fringe_images: + lt.info("Fringe image output turned off; skipping output of projected and captured fringe images.") + else: + # 2. Save projected sinusoidal fringe images to PNG format + # ======================================================== + fringes = Fringes(measurement.fringe_periods_x, measurement.fringe_periods_y) + images = fringes.get_frames( + 640, 320, "uint8", [0, 255] + ) # writes images we projected from sofast projector to disk + dir_save_cur = join(dir_save, "B1_projected_fringes") + ft.create_directories_if_necessary(dir_save_cur) + # Save y images + for idx_image in range(measurement.num_y_ims): + image = images[..., idx_image] + imageio.imwrite(join(dir_save_cur, output_file_prefix + f"y_{idx_image:02d}.png"), image) + # Save x images + for idx_image in range(measurement.num_x_ims): + image = images[..., measurement.num_y_ims + idx_image] + imageio.imwrite(join(dir_save_cur, output_file_prefix + f"x_{idx_image:02d}.png"), image) + + # 3. Save captured sinusoidal fringe images and mask images to PNG format + # ======================================================================= + dir_save_cur = join(dir_save, "B2_captured_fringes") + ft.create_directories_if_necessary(dir_save_cur) + + # Save mask (like a pixel mask value (all 0s, all 255s)) images + for idx_image in [0, 1]: + image = measurement.mask_images[..., idx_image] + imageio.imwrite(join(dir_save_cur, output_file_prefix + f"mask_{idx_image:02d}.png"), image) + # Save y images (when lines were vertical, e.g.) + for idx_image in range(measurement.num_y_ims): + image = measurement.fringe_images_y[..., idx_image] + imageio.imwrite(join(dir_save_cur, output_file_prefix + f"y_{idx_image:02d}.png"), image) + # Save x images (when lines were horizontal, e.g.) + for idx_image in range(measurement.num_x_ims): + image = measurement.fringe_images_x[..., idx_image] + imageio.imwrite(join(dir_save_cur, output_file_prefix + f"x_{idx_image:02d}.png"), image) # 4. Processes data with Sofast and save processed data to HDF5 # ============================================================= @@ -133,7 +151,20 @@ def process_single_facet( ft.create_directories_if_necessary(dir_save_cur) # Define surface definition (parabolic surface), this is the mirror - surface = Surface2DParabolic(initial_focal_lengths_xy=(300.0, 300.0), robust_least_squares=True, downsample=10) + if reference_mirror_surface_type == SYMMETRIC_PARABOLOID: + fit_surface = Surface2DParabolic( + initial_focal_lengths_xy=(fit_initial_focal_length_x, fit_initial_focal_length_y), + robust_least_squares=fit_robust_least_squares, + downsample=fit_downsample, + ) + elif reference_mirror_surface_type == PLANO: + fit_surface = Surface2DPlano(robust_least_squares=fit_robust_least_squares, downsample=fit_downsample) + else: + lt.error_and_raise( + ValueError, + f'Reference mirror surface type {reference_mirror_surface_type} is not one of ["{PLANO}", "{SYMMETRIC_PARABOLOID}"].', + ) + fit_surface = None # Eliminate Pylint error message. Never executes. # Calibrate fringes - (aka sinosoidal image) measurement.calibrate_fringe_images(calibration) @@ -142,7 +173,7 @@ def process_single_facet( sofast = Sofast(measurement, orientation, camera, display) # Process - sofast.process_optic_singlefacet(facet_data, surface) + sofast.process_optic_singlefacet(facet_data, fit_surface) # Get measurement statistics config = SofastConfiguration() @@ -163,7 +194,18 @@ def process_single_facet( # Get measured and reference optics mirror_measured = sofast.get_optic().mirror.no_parent_copy() - mirror_reference = MirrorParametric.generate_symmetric_paraboloid(100, mirror_measured.region) + if reference_mirror_surface_type == SYMMETRIC_PARABOLOID: + mirror_reference = MirrorParametric.generate_symmetric_paraboloid( + reference_mirror_focal_length, mirror_measured.region + ) + elif reference_mirror_surface_type == PLANO: + mirror_reference = MirrorParametric.generate_flat(mirror_measured.region) + else: + lt.error_and_raise( + ValueError, + f'Reference mirror surface type {reference_mirror_surface_type} is not one of ["{PLANO}", "{SYMMETRIC_PARABOLOID}"].', + ) + mirror_reference = None # Eliminate Pylint error message. Never executes. # Save optic objects and output destination plots.optic_measured = mirror_measured @@ -216,6 +258,18 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v # Strings denoting computation. measurement_id = "Time_Mirror_InstrumentMode" post_process_id = "PostSpec" + # Analysis control + fit_initial_focal_length_x = 300.0 + fit_initial_focal_length_y = 300.0 + fit_robust_least_squares = True + fit_downsample = 10 + # Reference mirror surface + reference_mirror_surface_type = ( + "symmetric_paraboloid" # Values from MirrorParametric.py: PLANO or SYMMETRIC_PARABOLOID + ) + reference_mirror_focal_length = 100.0 + # Output rendering control + output_fringe_images = True # Set plot control parameters to the values we want for the default example. plots.options_slope_vis.clim = 7 plots.options_slope_vis.resolution = 0.001 @@ -225,12 +279,10 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v plots.options_ray_trace_vis.enclosed_energy_max_semi_width = 1 plots.options_file_output.to_save = True plots.options_file_output.number_in_name = False - # Define viewing/illumination geometry v_target_center = Vxyz((0, 0, 100)) v_target_normal = Vxyz((0, 0, -1)) source = LightSourceSun.from_given_sun_position(Uxyz((0, 0, -1)), resolution=40) - # Define ray trace parameters plots.params_ray_trace.source = source plots.params_ray_trace.v_target_center = v_target_center @@ -250,6 +302,9 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v verbose = verbose_setting else: verbose = verbose_param + # Strings denoting computation + measurement_id = st.verify_contiguous(settings["Default"]["measurement_id"]) + post_process_id = st.verify_contiguous(settings["Default"]["post_process_id"]) # Input files file_camera = settings["Default"]["file_camera"] file_display = settings["Default"]["file_display"] @@ -258,10 +313,26 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v file_calibration = settings["Default"]["file_calibration"] file_measurement = settings["Default"]["file_measurement"] # Define save dir - dir_save = settings["Default"]["dir_save"] - # Strings denoting computation - measurement_id = st.verify_contiguous(settings["Default"]["measurement_id"]) - post_process_id = st.verify_contiguous(settings["Default"]["post_process_id"]) + dir_save_root = settings["Default"]["dir_save_root"] + dir_save = dir_save_root + '_' + post_process_id + # Analysis control + fit_initial_focal_length_x = float(settings["Default"]["fit_initial_focal_length_x"]) + fit_initial_focal_length_y = float(settings["Default"]["fit_initial_focal_length_y"]) + fit_robust_least_squares = st.convert_true_false_string_to_boolean( + settings["Default"]["fit_robust_least_squares"] + ) + fit_downsample = int(settings["Default"]["fit_downsample"]) + # Reference mirror surface + reference_mirror_surface_type = str(settings["Default"]["reference_mirror_surface_type"]) + if reference_mirror_surface_type == PLANO: + reference_mirror_focal_length = None + else: + reference_mirror_focal_length = float(settings["Default"]["reference_mirror_focal_length"]) + # Output rendering control + if "output_fringe_images" in settings["Default"]: + output_fringe_images = st.convert_true_false_string_to_boolean(settings["Default"]["output_fringe_images"]) + else: + output_fringe_images = True # Set plot control parameters plots.set_plot_control_from_settings(settings) @@ -288,21 +359,43 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v lt.info('dir_save = ' + str(dir_save)) lt.info('measurement_id = ' + str(measurement_id)) lt.info('post_process_id = ' + str(post_process_id)) + lt.info('dir_save = ' + str(dir_save)) + lt.info('measurement_id = ' + str(measurement_id)) + lt.info('post_process_id = ' + str(post_process_id)) + lt.info('fit_initial_focal_length_x = ' + str(fit_initial_focal_length_x)) + lt.info('fit_initial_focal_length_y = ' + str(fit_initial_focal_length_y)) + lt.info('fit_robust_least_squares = ' + str(fit_robust_least_squares)) + lt.info('fit_downsample = ' + str(fit_downsample)) + lt.info('reference_mirror_surface_type = ' + str(reference_mirror_surface_type)) + lt.info('reference_mirror_focal_length = ' + str(reference_mirror_focal_length)) + lt.info('output_fringe_images = ' + str(output_fringe_images)) if verbose: lt.info('Calling routine example_process_single_facet(...)...') # Process and output process_single_facet( verbose, + # Input data file_camera, file_display, file_orientation, file_facet, file_calibration, file_measurement, + # Output file control dir_save, measurement_id, post_process_id, + # Analysis control + fit_initial_focal_length_x, + fit_initial_focal_length_y, + fit_robust_least_squares, + fit_downsample, + # Reference mirror surface + reference_mirror_surface_type, + reference_mirror_focal_length, + # Output rendering control + output_fringe_images, plots, ) diff --git a/opencsp/common/lib/csp/MirrorParametric.py b/opencsp/common/lib/csp/MirrorParametric.py index 68da747d6..af8523361 100644 --- a/opencsp/common/lib/csp/MirrorParametric.py +++ b/opencsp/common/lib/csp/MirrorParametric.py @@ -17,6 +17,10 @@ from opencsp.common.lib.geometry.Vxy import Vxy from opencsp.common.lib.geometry.FunctionXYContinuous import FunctionXYContinuous +# Mirror surface types +PLANO = "plano" +SYMMETRIC_PARABOLOID = "symmetric_paraboloid" + class MirrorParametric(MirrorAbstract): """ From f8130b74807ae327e83ef4e4e01ac789f6a4f43a Mon Sep 17 00:00:00 2001 From: rcbrost Date: Thu, 22 Jan 2026 16:59:36 -0700 Subject: [PATCH 26/29] Fixed StandardPlotOutput.py pytest failure. --- opencsp/common/lib/csp/StandardPlotOutput.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opencsp/common/lib/csp/StandardPlotOutput.py b/opencsp/common/lib/csp/StandardPlotOutput.py index 3ab12370b..e467ef02c 100644 --- a/opencsp/common/lib/csp/StandardPlotOutput.py +++ b/opencsp/common/lib/csp/StandardPlotOutput.py @@ -34,7 +34,7 @@ class _OptionsFileOutput: """To close figures after save. (default False)""" number_in_name: bool = True """To keep figure number in save name. (default True)""" - file_prefix: str = None + file_prefix: str = '' """String to prefix each output file, including separator""" From 3a4f36fbd2d813596a1be5997470050ad3627f1b Mon Sep 17 00:00:00 2001 From: rcbrost Date: Thu, 22 Jan 2026 17:33:06 -0700 Subject: [PATCH 27/29] Verified that pytest examples run as expected, and made progress on example_process_single_facet_README.md file. --- .../example_scene_reconstruction_README.md | 4 +- .../example_process_single_facet_README.md | 79 +++++++++++++++++-- 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/example/scene_reconstruction/example_scene_reconstruction_README.md b/example/scene_reconstruction/example_scene_reconstruction_README.md index 69c4b82a5..81c5943a3 100644 --- a/example/scene_reconstruction/example_scene_reconstruction_README.md +++ b/example/scene_reconstruction/example_scene_reconstruction_README.md @@ -11,11 +11,11 @@ To run this as a pytest on the built-in input: or pytest scene_reconstruction\example_scene_reconstruction.py -To run this on the default built-in input, omit the -s option: +To run this on the default built-in input: 1. cd to the directory containing the script "example_scene_reconstruction.py". 2. Run the script: python example_scene_reconstruction.py --verbose - The "--verbose" flag is optional.' + The "--verbose" flag is optional. To run this on new input, use the -s option and point to a settings control file. For an example settings file, see: diff --git a/example/sofast_fringe/single_facet/example_process_single_facet_README.md b/example/sofast_fringe/single_facet/example_process_single_facet_README.md index ef94b7e6f..83623e77f 100644 --- a/example/sofast_fringe/single_facet/example_process_single_facet_README.md +++ b/example/sofast_fringe/single_facet/example_process_single_facet_README.md @@ -4,23 +4,89 @@ Example SOFAST data processing for a single facet measurement. Given a stored measurement file from a SOFAST data acquisition, process the file to construct a slope map and generate the standard plot output suite. +Run Pytest +---------- + To run this as a pytest on the built-in input: 1. cd to the OpenCSP code directory. 2. cd to the example subdirectory. 3. Execute pytest: pytest or - pytest sofast_fringe\example_process_single_facet.py + pytest sofast_fringe\single_facet\example_process_single_facet.py + +Default Run on Built-In Data +---------------------------- -To run this on the default built-in input, omit the -s option: +To run this on the default built-in input: 1. cd to the directory containing the script "example_process_single_facet.py". 2. Run the script: python example_process_single_facet.py --verbose - The "--verbose" flag is optional.' + The "--verbose" flag is optional. + +This runs the code on data that is built into the repository. To avoid bloating +the repository, this input data has been downsampled to reduce its size. + + +Run on Other Data +----------------- To run this on new input, use the -s option and point to a settings control file. -For an example settings file, see: - \example\process_single_facet\example_process_single_facet_settings_ctemp.ini +For an example settings file designed for exploration, see: + + \example\sofast_fringe\single_facet\ + +There are several files there with a ".ini" extension, which execute the code in +different scenarios. + + +Local Exploration Using OpenCSP Example Data +-------------------------------------------- + + + + dir: \example\sofast_fringe\single_facet\ + file: ????_Ctemp.ini + +Note this files has a "ctemp" suffix, indicating that they assume a local directory "C:\ctemp\" +which contains the test data published on OpenCSP. This enables you to execute exploratory runs +on your local computer, without any modifications to the script. + + +Analyzing Data with an Archival Directory Scheme +------------------------------------------------ + +To run the single-facet SOFAST processing on a directory structure designed for ongoing measurement +and archival of results, see the file: + + dir: \example\sofast_fringe\single_facet\ + file: 20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini + +This file contains instructions explaining how to configure the directories and data files, and then +how to execute the example_process_single_facet.py file on that scenario. + +For variations of the default scenario that run faster, against different reference mirrors, etc, +see the variation files: + + dir: \example\sofast_fringe\single_facet\ + file: 20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini + file: + file: + +Note that these files have a different "_p00x_xx_" substring, which is the post_process_id denoting +a particular set of processing and output settings. This provides a means for running the code +under different settings, adn then comparing the results. + +Also note that the files have a "Q" suffix, indicating that they assume a mapped drive "Q:". +This enables you to put the input and output data in your preferred location, map the Q: drive +letter to that location, and then execute the example script without modification. + + +Analyzing Your New Data +----------------------- + + + We recommend copying this file and placing it alongside the data you wish to run. For example, to run the full-size OpenCSP example:' @@ -43,6 +109,9 @@ For example, to run the full-size OpenCSP example:' created alongside the input directory. (This is to distinguish it from output from other examples within the directory.) +For an example settings file designed for measuring and logging results over time, see: + \example\sofast_fringe\single_facet\20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini + To run this calculation on your own data: A. Create a directory holding your input data. B. Copy the "example_process_single_facet_settings_ctemp.ini" file to a new name, such as From 4a0ec21a0f9c0359bc631c878e3a120459b26711 Mon Sep 17 00:00:00 2001 From: rcbrost Date: Fri, 23 Jan 2026 08:04:54 -0700 Subject: [PATCH 28/29] Added astigmatic reference paraboloid to MirrorParametric.py and example_process_single_facet.py. --- ...efault_process_single_facet_settings_Q.ini | 10 +- ...2_fast_process_single_facet_settings_Q.ini | 10 +- ...fy25m_process_single_facet_settings_Q.ini} | 12 +- ..._plano_process_single_facet_settings_Q.ini | 10 +- ...fy400m_process_single_facet_settings_Q.ini | 112 ++++++++++++++++++ .../example_process_single_facet.py | 40 ++++--- .../example_process_single_facet_README.md | 47 ++++++-- opencsp/common/lib/csp/MirrorParametric.py | 32 ++++- opencsp/common/lib/tool/string_tools.py | 27 +++++ 9 files changed, 261 insertions(+), 39 deletions(-) rename example/sofast_fringe/single_facet/{20250818_163443_SNLTF-A_OLSLrsqw_p003_ref_25m_process_single_facet_settings_Q.ini => 20250818_163443_SNLTF-A_OLSLrsqw_p003_refx25m_refy25m_process_single_facet_settings_Q.ini} (92%) create mode 100644 example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p005_refx100m_refy400m_process_single_facet_settings_Q.ini diff --git a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini index 459341d5e..0275a6bdd 100644 --- a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini +++ b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini @@ -144,11 +144,15 @@ fit_robust_least_squares = True fit_downsample = 10 # Reference mirror -# As of this writing, surface choices are "plano" and "symmetric_paraboloid". +# As of this writing, surface choices are "symmetric_paraboloid", "astigmatic_paraboloid", and "plano". # Distance values are meters. -#reference_mirror_surface = plano reference_mirror_surface_type = symmetric_paraboloid -reference_mirror_focal_length = 100 +reference_mirror_focal_length_x = 100.0 +reference_mirror_focal_length_y = None +#reference_mirror_surface_type = astigmatic_paraboloid +#reference_mirror_focal_length_x = 100.0 +#reference_mirror_focal_length_y = 400.0 +#reference_mirror_surface_type = plano # Output control # Output control flags are optional and may be omitted if brevity is desired. diff --git a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini index aa75729b1..769f11016 100644 --- a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini +++ b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini @@ -38,11 +38,15 @@ fit_robust_least_squares = True fit_downsample = 10 # Reference mirror -# As of this writing, surface choices are "plano" and "symmetric_paraboloid". +# As of this writing, surface choices are "symmetric_paraboloid", "astigmatic_paraboloid", and "plano". # Distance values are meters. -#reference_mirror_surface = plano reference_mirror_surface_type = symmetric_paraboloid -reference_mirror_focal_length = 100 +reference_mirror_focal_length_x = 100.0 +reference_mirror_focal_length_y = None +#reference_mirror_surface_type = astigmatic_paraboloid +#reference_mirror_focal_length_x = 100.0 +#reference_mirror_focal_length_y = 400.0 +#reference_mirror_surface_type = plano # Output control # Output control flags are optional and may be omitted if brevity is desired. diff --git a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p003_ref_25m_process_single_facet_settings_Q.ini b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p003_refx25m_refy25m_process_single_facet_settings_Q.ini similarity index 92% rename from example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p003_ref_25m_process_single_facet_settings_Q.ini rename to example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p003_refx25m_refy25m_process_single_facet_settings_Q.ini index d70dd7c04..21f9e1b25 100644 --- a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p003_ref_25m_process_single_facet_settings_Q.ini +++ b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p003_refx25m_refy25m_process_single_facet_settings_Q.ini @@ -16,7 +16,7 @@ verbose = True # Strings to denote this computation. Must be contiguous, with no space, tabs or line feeds. measurement_id = 20250818_163443_SLTF-A_OLSLrsqw -post_process_id = p003_ref_25m +post_process_id = p003_refx25m_refy25m # Input file_camera = Q:\Instruments\OLSL_OpticsLabSofastLandscape\camera_sofast_optics_lab_landscape_2025_02.h5 @@ -38,11 +38,15 @@ fit_robust_least_squares = True fit_downsample = 10 # Reference mirror -# As of this writing, surface choices are "plano" and "symmetric_paraboloid". +# As of this writing, surface choices are "symmetric_paraboloid", "astigmatic_paraboloid", and "plano". # Distance values are meters. -#reference_mirror_surface = plano reference_mirror_surface_type = symmetric_paraboloid -reference_mirror_focal_length = 25 +reference_mirror_focal_length_x = 25.0 +reference_mirror_focal_length_y = None +#reference_mirror_surface_type = astigmatic_paraboloid +#reference_mirror_focal_length_x = 100.0 +#reference_mirror_focal_length_y = 400.0 +#reference_mirror_surface_type = plano # Output control # Output control flags are optional and may be omitted if brevity is desired. diff --git a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p004_ref_plano_process_single_facet_settings_Q.ini b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p004_ref_plano_process_single_facet_settings_Q.ini index 51ff9884b..f9684914e 100644 --- a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p004_ref_plano_process_single_facet_settings_Q.ini +++ b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p004_ref_plano_process_single_facet_settings_Q.ini @@ -38,11 +38,15 @@ fit_robust_least_squares = True fit_downsample = 10 # Reference mirror -# As of this writing, surface choices are "plano" and "symmetric_paraboloid". +# As of this writing, surface choices are "symmetric_paraboloid", "astigmatic_paraboloid", and "plano". # Distance values are meters. -#reference_mirror_surface = plano +#reference_mirror_surface_type = symmetric_paraboloid +#reference_mirror_focal_length_x = 100.0 +#reference_mirror_focal_length_y = None +#reference_mirror_surface_type = astigmatic_paraboloid +#reference_mirror_focal_length_x = 100.0 +#reference_mirror_focal_length_y = 400.0 reference_mirror_surface_type = plano -reference_mirror_focal_length = None # Output control # Output control flags are optional and may be omitted if brevity is desired. diff --git a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p005_refx100m_refy400m_process_single_facet_settings_Q.ini b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p005_refx100m_refy400m_process_single_facet_settings_Q.ini new file mode 100644 index 000000000..c71a666d9 --- /dev/null +++ b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p005_refx100m_refy400m_process_single_facet_settings_Q.ini @@ -0,0 +1,112 @@ +# This file provides the data paths and control parameters for the example_process_single_facet.py example. +# +# This file demonstrates a scenario where a systematic file and directory convention is used to track +# multiple measurements of multiple mirrors, in a facility with multiple optical metrology setups, +# and under multiple settings of analysis parameters and output control. +# +# For detailed instructions on the context, see the default processing file: +# 20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini +# +# This version is modified to compare against a reference mirror with a 25 m focal length. +# + +[Default] +# Whether to output detailed progress output, intermediate calculation results, etc. +verbose = True + +# Strings to denote this computation. Must be contiguous, with no space, tabs or line feeds. +measurement_id = 20250818_163443_SLTF-A_OLSLrsqw +post_process_id = p005_refx100m_refy400m + +# Input +file_camera = Q:\Instruments\OLSL_OpticsLabSofastLandscape\camera_sofast_optics_lab_landscape_2025_02.h5 +file_orientation = Q:\Instruments\OLSL_OpticsLabSofastLandscape\spatial_orientation_optics_lab_landscape.h5 +file_display = Q:\Instruments\OLSL_OpticsLabSofastLandscape\OLSLrsqw_OpticsLabSofastLandscapeFringeSquareWhite\display_shape_optics_lab_landscape_square_distorted_3d_100x100.h5 +file_facet = Q:\Mirrors\SNLTF\0_Common\Facet_NSTTF.json +file_calibration = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A1_intensity_response\image_calibration_scaling_20250818_163358.h5 +file_measurement = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A2_collection_result\20250818_163443_measurement_fringe.h5 + +# Directory to write output files. +# (The post_process_id below will be added as a suffix.) +dir_save_root = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post + +# Analysis control parameters +# Distance values are meters. +fit_initial_focal_length_x = 300.0 +fit_initial_focal_length_y = 300.0 +fit_robust_least_squares = True +fit_downsample = 10 + +# Reference mirror +# As of this writing, surface choices are "symmetric_paraboloid", "astigmatic_paraboloid", and "plano". +# Distance values are meters. +#reference_mirror_surface_type = symmetric_paraboloid +#reference_mirror_focal_length_x = 100.0 +#reference_mirror_focal_length_y = None +reference_mirror_surface_type = astigmatic_paraboloid +reference_mirror_focal_length_x = 100.0 +reference_mirror_focal_length_y = 400.0 +#reference_mirror_surface_type = plano + +# Output control +# Output control flags are optional and may be omitted if brevity is desired. +# --------------------------------------------------------------------------- +output_fringe_images = True + +# Plot control parameters +# For detaled definitions and default values, see StandardPlotOutput.py, particularly the classes +# _OptionsFileOutput, _OptionsSlopeVis, _OptionsSlopeDeviationVis, _OptionsCurvatureVis, _OptionsRayTraceVis, and _RayTraceParameters. +# +# Plot control flags are optional and may be omitted if brevity is desired. +# ------------------------------------------------------------------------- +# File output control +plots.options_file_output.to_save = True +plots.options_file_output.save_dpi = 200 +plots.options_file_output.save_format = png +plots.options_file_output.close_after_save = False +plots.options_file_output.number_in_name = False + +# Slope plot control +plots.options_slope_vis.resolution = 0.001 +plots.options_slope_vis.clim = 7 +plots.options_slope_vis.quiver_density = 0.1 +plots.options_slope_vis.quiver_scale = 25 +plots.options_slope_vis.quiver_color = white +plots.options_slope_vis.to_plot = True + +# Slope deviation plot control +plots.options_slope_deviation_vis.resolution = 0.001 +plots.options_slope_deviation_vis.clim = 1.5 +plots.options_slope_deviation_vis.quiver_density = 0.1 +plots.options_slope_deviation_vis.quiver_scale = 25 +plots.options_slope_deviation_vis.quiver_color = white +plots.options_slope_deviation_vis.to_plot = True + +# Curvature plot control +plots.options_curvature_vis.resolution = 0.001 +plots.options_curvature_vis.clim = 50 +plots.options_curvature_vis.processing_1 = None +plots.options_curvature_vis.processing_2 = None +plots.options_curvature_vis.smooth_kernel_width = 1 +plots.options_curvature_vis.to_plot = True + +# Ray trace plot control +plots.options_ray_trace_vis.ray_trace_optic_res = 0.05 +plots.options_ray_trace_vis.hist_bin_res = 0.07 +plots.options_ray_trace_vis.hist_extent = 3 +plots.options_ray_trace_vis.enclosed_energy_max_semi_width = 1 +plots.options_ray_trace_vis.to_plot = True + +# Ray trace parameters +plots.options_ray_trace_vis.sun_direction_x = 0 +plots.options_ray_trace_vis.sun_direction_y = 0 +plots.options_ray_trace_vis.sun_direction_z = -1 +plots.options_ray_trace_vis.sun_sample_resolution = 40 + +plots.options_ray_trace_vis.v_target_center_x = 0 +plots.options_ray_trace_vis.v_target_center_y = 0 +plots.options_ray_trace_vis.v_target_center_z = 100 + +plots.options_ray_trace_vis.v_target_normal_x = 0 +plots.options_ray_trace_vis.v_target_normal_y = 0 +plots.options_ray_trace_vis.v_target_normal_z = -1 diff --git a/example/sofast_fringe/single_facet/example_process_single_facet.py b/example/sofast_fringe/single_facet/example_process_single_facet.py index 61d21949b..fcc3a440a 100644 --- a/example/sofast_fringe/single_facet/example_process_single_facet.py +++ b/example/sofast_fringe/single_facet/example_process_single_facet.py @@ -44,7 +44,7 @@ from opencsp.app.sofast.lib.SpatialOrientation import SpatialOrientation from opencsp.common.lib.camera.Camera import Camera from opencsp.common.lib.csp.LightSourceSun import LightSourceSun -from opencsp.common.lib.csp.MirrorParametric import MirrorParametric, PLANO, SYMMETRIC_PARABOLOID +from opencsp.common.lib.csp.MirrorParametric import MirrorParametric, SYMMETRIC_PARABOLOID, ASTIGMATIC_PARABOLOID, PLANO from opencsp.common.lib.csp.StandardPlotOutput import StandardPlotOutput from opencsp.common.lib.deflectometry.Surface2DParabolic import Surface2DParabolic from opencsp.common.lib.deflectometry.Surface2DPlano import Surface2DPlano @@ -76,7 +76,8 @@ def process_single_facet( fit_downsample: int, # Reference mirror surface reference_mirror_surface_type: str, - reference_mirror_focal_length: float, + reference_mirror_focal_length_x: float, + reference_mirror_focal_length_y: float, # Output rendering control output_fringe_images: bool, plots: StandardPlotOutput, @@ -151,7 +152,9 @@ def process_single_facet( ft.create_directories_if_necessary(dir_save_cur) # Define surface definition (parabolic surface), this is the mirror - if reference_mirror_surface_type == SYMMETRIC_PARABOLOID: + if (reference_mirror_surface_type == SYMMETRIC_PARABOLOID) or ( + reference_mirror_surface_type == ASTIGMATIC_PARABOLOID + ): fit_surface = Surface2DParabolic( initial_focal_lengths_xy=(fit_initial_focal_length_x, fit_initial_focal_length_y), robust_least_squares=fit_robust_least_squares, @@ -162,7 +165,7 @@ def process_single_facet( else: lt.error_and_raise( ValueError, - f'Reference mirror surface type {reference_mirror_surface_type} is not one of ["{PLANO}", "{SYMMETRIC_PARABOLOID}"].', + f'Reference mirror surface type {reference_mirror_surface_type} is not one of ["{SYMMETRIC_PARABOLOID}", "{ASTIGMATIC_PARABOLOID}, "{PLANO}"].', ) fit_surface = None # Eliminate Pylint error message. Never executes. @@ -196,14 +199,18 @@ def process_single_facet( mirror_measured = sofast.get_optic().mirror.no_parent_copy() if reference_mirror_surface_type == SYMMETRIC_PARABOLOID: mirror_reference = MirrorParametric.generate_symmetric_paraboloid( - reference_mirror_focal_length, mirror_measured.region + reference_mirror_focal_length_x, mirror_measured.region + ) + elif reference_mirror_surface_type == ASTIGMATIC_PARABOLOID: + mirror_reference = MirrorParametric.generate_astigmatic_xy_paraboloid( + reference_mirror_focal_length_x, reference_mirror_focal_length_y, mirror_measured.region ) elif reference_mirror_surface_type == PLANO: mirror_reference = MirrorParametric.generate_flat(mirror_measured.region) else: lt.error_and_raise( ValueError, - f'Reference mirror surface type {reference_mirror_surface_type} is not one of ["{PLANO}", "{SYMMETRIC_PARABOLOID}"].', + f'Reference mirror surface type {reference_mirror_surface_type} is not one of ["{SYMMETRIC_PARABOLOID}", "{ASTIGMATIC_PARABOLOID}, "{PLANO}"].', ) mirror_reference = None # Eliminate Pylint error message. Never executes. @@ -232,7 +239,7 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v verbose : bool If true, output detailed progress and calculation output. """ - # Setup plot control, whcih might have some values set from settings, if provided. + # Setup plot control, which might have some values set from settings, if provided. plots = StandardPlotOutput() # Get settings @@ -264,10 +271,9 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v fit_robust_least_squares = True fit_downsample = 10 # Reference mirror surface - reference_mirror_surface_type = ( - "symmetric_paraboloid" # Values from MirrorParametric.py: PLANO or SYMMETRIC_PARABOLOID - ) - reference_mirror_focal_length = 100.0 + reference_mirror_surface_type = SYMMETRIC_PARABOLOID # Strings from MirrorParametric.py: SYMMETRIC_PARABOLOID, ASTIGMATIC_PARABOLOID, or PLANO + reference_mirror_focal_length_x = 100.0 # Ignored if plano + reference_mirror_focal_length_y = 100.0 # Ignored if plano or symmetric paraboloid # Output rendering control output_fringe_images = True # Set plot control parameters to the values we want for the default example. @@ -325,9 +331,11 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v # Reference mirror surface reference_mirror_surface_type = str(settings["Default"]["reference_mirror_surface_type"]) if reference_mirror_surface_type == PLANO: - reference_mirror_focal_length = None + reference_mirror_focal_length_x = None + reference_mirror_focal_length_y = None else: - reference_mirror_focal_length = float(settings["Default"]["reference_mirror_focal_length"]) + reference_mirror_focal_length_x = st.float_or_none(settings["Default"]["reference_mirror_focal_length_x"]) + reference_mirror_focal_length_y = st.float_or_none(settings["Default"]["reference_mirror_focal_length_y"]) # Output rendering control if "output_fringe_images" in settings["Default"]: output_fringe_images = st.convert_true_false_string_to_boolean(settings["Default"]["output_fringe_images"]) @@ -367,7 +375,8 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v lt.info('fit_robust_least_squares = ' + str(fit_robust_least_squares)) lt.info('fit_downsample = ' + str(fit_downsample)) lt.info('reference_mirror_surface_type = ' + str(reference_mirror_surface_type)) - lt.info('reference_mirror_focal_length = ' + str(reference_mirror_focal_length)) + lt.info('reference_mirror_focal_length_x = ' + str(reference_mirror_focal_length_x)) + lt.info('reference_mirror_focal_length_y = ' + str(reference_mirror_focal_length_y)) lt.info('output_fringe_images = ' + str(output_fringe_images)) if verbose: lt.info('Calling routine example_process_single_facet(...)...') @@ -393,7 +402,8 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v fit_downsample, # Reference mirror surface reference_mirror_surface_type, - reference_mirror_focal_length, + reference_mirror_focal_length_x, + reference_mirror_focal_length_y, # Output rendering control output_fringe_images, plots, diff --git a/example/sofast_fringe/single_facet/example_process_single_facet_README.md b/example/sofast_fringe/single_facet/example_process_single_facet_README.md index 83623e77f..5899b8e62 100644 --- a/example/sofast_fringe/single_facet/example_process_single_facet_README.md +++ b/example/sofast_fringe/single_facet/example_process_single_facet_README.md @@ -4,6 +4,8 @@ Example SOFAST data processing for a single facet measurement. Given a stored measurement file from a SOFAST data acquisition, process the file to construct a slope map and generate the standard plot output suite. +This file can be run in several different modes, explained below. + Run Pytest ---------- @@ -15,6 +17,7 @@ To run this as a pytest on the built-in input: or pytest sofast_fringe\single_facet\example_process_single_facet.py + Default Run on Built-In Data ---------------------------- @@ -31,17 +34,41 @@ the repository, this input data has been downsampled to reduce its size. Run on Other Data ----------------- -To run this on new input, use the -s option and point to a settings control file. -For an example settings file designed for exploration, see: +The built-in data described above enables automatic testing to verify code execution, +but the data is heavily downsampled and thus not representative of realistic output. +Also the built-in input and output files are in obscure locations that don't correspond +to how one would organize data when using SOFAST in practice. + +The example_process_single_facet.py file provides a -s option that points to a settings +control file. This text file is easy to edit, and enables you to point to your preferred +location of input data, and also direct output to your preferred location. You can also +control program execution and output simply by editing the settings file. This avoids +the need to modify source code simply to run a new problem (which is highly discouraged). + +You can easily create a settings file for your data, with your run preferences, +and then execute it without modifying the example_process_single_facet.py file or +other code. + +To see example settings files, see: \example\sofast_fringe\single_facet\ There are several files there with a ".ini" extension, which execute the code in -different scenarios. +different scenarios. These scenarios include information exploration on your local +computer, or more organized measurement campaigns either on your local computer +or in a network server environment. + +OpenCSP provides full-size example data which can be used for both of these scenarios, +enabling you to learn how the code should run and what its output should look like. +This experience will give you familiarity with the process when you run the code on +your own measurement data. Below we explain how to use these settings and example +data files for both informal exploration and a more structured measurement campaign. -Local Exploration Using OpenCSP Example Data --------------------------------------------- +Local Exploration +----------------- + +For an example settings file designed for exploration, see: @@ -70,12 +97,12 @@ see the variation files: dir: \example\sofast_fringe\single_facet\ file: 20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini - file: - file: + file: 20250818_163443_SNLTF-A_OLSLrsqw_p003_ref_25m_process_single_facet_settings_Q.ini + file: 20250818_163443_SNLTF-A_OLSLrsqw_p004_ref_plano_process_single_facet_settings_Q.ini -Note that these files have a different "_p00x_xx_" substring, which is the post_process_id denoting -a particular set of processing and output settings. This provides a means for running the code -under different settings, adn then comparing the results. +Note that these files each have a different "_p00x_xx_" substring, which is the "post_process_id" +denoting a particular set of processing and output settings. This provides a means for running +the code under different settings, and then comparing the results. Also note that the files have a "Q" suffix, indicating that they assume a mapped drive "Q:". This enables you to put the input and output data in your preferred location, map the Q: drive diff --git a/opencsp/common/lib/csp/MirrorParametric.py b/opencsp/common/lib/csp/MirrorParametric.py index af8523361..255a736e6 100644 --- a/opencsp/common/lib/csp/MirrorParametric.py +++ b/opencsp/common/lib/csp/MirrorParametric.py @@ -18,8 +18,9 @@ from opencsp.common.lib.geometry.FunctionXYContinuous import FunctionXYContinuous # Mirror surface types -PLANO = "plano" SYMMETRIC_PARABOLOID = "symmetric_paraboloid" +ASTIGMATIC_PARABOLOID = "astigmatic_paraboloid" +PLANO = "plano" class MirrorParametric(MirrorAbstract): @@ -148,6 +149,35 @@ def surface_function(x, y): return cls(surface_function, shape) + @classmethod + def generate_astigmatic_xy_paraboloid( + cls, focal_length_x: float, focal_length_y: float, shape: RegionXY + ) -> "MirrorParametric": + """Generate an astigmatic parabolic mirror with the given focal lengths in x and y. + Rotation of astigmatism is axis-aligned with the x and y axes. + + Parameters + ---------- + focal_length_x : float + Focal length in x direction + focal_length_y : float + Focal length in y direction + shape : RegionXY + Mirror top-down region. + + Returns + ------- + MirrorParametric + """ + # Create surface function + a = 1.0 / (4 * focal_length_x) + b = 1.0 / (4 * focal_length_y) + + def surface_function(x, y): + return (a * (x**2)) + (b * (y**2)) + + return cls(surface_function, shape) + @classmethod def generate_flat(cls, shape: RegionXY) -> "MirrorParametric": """Generate a flat, z=0 mirror diff --git a/opencsp/common/lib/tool/string_tools.py b/opencsp/common/lib/tool/string_tools.py index 95cdeba5f..d158702a8 100644 --- a/opencsp/common/lib/tool/string_tools.py +++ b/opencsp/common/lib/tool/string_tools.py @@ -74,6 +74,33 @@ def convert_true_false_string_to_boolean(true_or_false_str: str) -> bool: lt.error_and_raise(ValueError, f'True/False string {true_or_false_str} is not "True" or "False"') +def float_or_none(num_or_none_str: str) -> bool: + """ + Accepts string with value either "None" or a valid floating-point number (e.g., "-1.234") + and returns either the corresponding number, or None. + Throws an error if not one of these two cases. + + Parameters + ---------- + num_or_none_str : str + The string to convert to floating point. Must be either a valid number, or "None". + + Returns + ------- + float | None + The corresponding value as a float type, or None. + """ + if num_or_none_str == "None": + return None + else: + try: + return float(num_or_none_str) + except ValueError: + lt.error_and_raise( + ValueError, f'Number/None string {num_or_none_str} is not either a valid number or "None"' + ) + + def verify_contiguous(token_str: str) -> str: """ Checks string to ensure that is a contiguous string with no white space, newlines, etc. From 81b4f042f818bf004458cb9b61e32ef50e61930a Mon Sep 17 00:00:00 2001 From: rcbrost Date: Fri, 23 Jan 2026 09:30:45 -0700 Subject: [PATCH 29/29] Added measured mirror interpolation control to example_process_single_facet.py. --- ...efault_process_single_facet_settings_Q.ini | 9 ++ ...2_fast_process_single_facet_settings_Q.ini | 9 ++ ...efy25m_process_single_facet_settings_Q.ini | 9 ++ ..._plano_process_single_facet_settings_Q.ini | 9 ++ ...fy400m_process_single_facet_settings_Q.ini | 9 ++ ...linear_process_single_facet_settings_Q.ini | 122 ++++++++++++++++++ .../example_process_single_facet.py | 15 ++- opencsp/common/lib/csp/MirrorPoint.py | 17 ++- 8 files changed, 193 insertions(+), 6 deletions(-) create mode 100644 example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p006_bilinear_process_single_facet_settings_Q.ini diff --git a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini index 0275a6bdd..33184d29f 100644 --- a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini +++ b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini @@ -143,6 +143,15 @@ fit_initial_focal_length_y = 300.0 fit_robust_least_squares = True fit_downsample = 10 +# Measured mirror point interpolation +# As of this writing, surface choices are "nearest", "bilinear", and "clough_tocher" +# "nearest" the default, and will fill the plot domain. +# "bilinear" plots only show mirror points observed. +# "clough_tocher" piecewise cubic, curvature-minimizing interpolation. +measured_interpolation_type = nearest +#measured_interpolation_type = bilinear +#measured_interpolation_type = clough_tocher + # Reference mirror # As of this writing, surface choices are "symmetric_paraboloid", "astigmatic_paraboloid", and "plano". # Distance values are meters. diff --git a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini index 769f11016..c50f9d16e 100644 --- a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini +++ b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini @@ -37,6 +37,15 @@ fit_initial_focal_length_y = 300.0 fit_robust_least_squares = True fit_downsample = 10 +# Measured mirror point interpolation +# As of this writing, surface choices are "nearest", "bilinear", and "clough_tocher" +# "nearest" the default, and will fill the plot domain. +# "bilinear" plots only show mirror points observed. +# "clough_tocher" piecewise cubic, curvature-minimizing interpolation. +measured_interpolation_type = nearest +#measured_interpolation_type = bilinear +#measured_interpolation_type = clough_tocher + # Reference mirror # As of this writing, surface choices are "symmetric_paraboloid", "astigmatic_paraboloid", and "plano". # Distance values are meters. diff --git a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p003_refx25m_refy25m_process_single_facet_settings_Q.ini b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p003_refx25m_refy25m_process_single_facet_settings_Q.ini index 21f9e1b25..d74c5716b 100644 --- a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p003_refx25m_refy25m_process_single_facet_settings_Q.ini +++ b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p003_refx25m_refy25m_process_single_facet_settings_Q.ini @@ -37,6 +37,15 @@ fit_initial_focal_length_y = 300.0 fit_robust_least_squares = True fit_downsample = 10 +# Measured mirror point interpolation +# As of this writing, surface choices are "nearest", "bilinear", and "clough_tocher" +# "nearest" the default, and will fill the plot domain. +# "bilinear" plots only show mirror points observed. +# "clough_tocher" piecewise cubic, curvature-minimizing interpolation. +measured_interpolation_type = nearest +#measured_interpolation_type = bilinear +#measured_interpolation_type = clough_tocher + # Reference mirror # As of this writing, surface choices are "symmetric_paraboloid", "astigmatic_paraboloid", and "plano". # Distance values are meters. diff --git a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p004_ref_plano_process_single_facet_settings_Q.ini b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p004_ref_plano_process_single_facet_settings_Q.ini index f9684914e..51f089f46 100644 --- a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p004_ref_plano_process_single_facet_settings_Q.ini +++ b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p004_ref_plano_process_single_facet_settings_Q.ini @@ -37,6 +37,15 @@ fit_initial_focal_length_y = 300.0 fit_robust_least_squares = True fit_downsample = 10 +# Measured mirror point interpolation +# As of this writing, surface choices are "nearest", "bilinear", and "clough_tocher" +# "nearest" the default, and will fill the plot domain. +# "bilinear" plots only show mirror points observed. +# "clough_tocher" piecewise cubic, curvature-minimizing interpolation. +measured_interpolation_type = nearest +#measured_interpolation_type = bilinear +#measured_interpolation_type = clough_tocher + # Reference mirror # As of this writing, surface choices are "symmetric_paraboloid", "astigmatic_paraboloid", and "plano". # Distance values are meters. diff --git a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p005_refx100m_refy400m_process_single_facet_settings_Q.ini b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p005_refx100m_refy400m_process_single_facet_settings_Q.ini index c71a666d9..63eb459c6 100644 --- a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p005_refx100m_refy400m_process_single_facet_settings_Q.ini +++ b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p005_refx100m_refy400m_process_single_facet_settings_Q.ini @@ -37,6 +37,15 @@ fit_initial_focal_length_y = 300.0 fit_robust_least_squares = True fit_downsample = 10 +# Measured mirror point interpolation +# As of this writing, surface choices are "nearest", "bilinear", and "clough_tocher" +# "nearest" the default, and will fill the plot domain. +# "bilinear" plots only show mirror points observed. +# "clough_tocher" piecewise cubic, curvature-minimizing interpolation. +measured_interpolation_type = nearest +#measured_interpolation_type = bilinear +#measured_interpolation_type = clough_tocher + # Reference mirror # As of this writing, surface choices are "symmetric_paraboloid", "astigmatic_paraboloid", and "plano". # Distance values are meters. diff --git a/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p006_bilinear_process_single_facet_settings_Q.ini b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p006_bilinear_process_single_facet_settings_Q.ini new file mode 100644 index 000000000..be1235973 --- /dev/null +++ b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p006_bilinear_process_single_facet_settings_Q.ini @@ -0,0 +1,122 @@ +# This file provides the data paths and control parameters for the example_process_single_facet.py example. +# +# This file demonstrates a scenario where a systematic file and directory convention is used to track +# multiple measurements of multiple mirrors, in a facility with multiple optical metrology setups, +# and under multiple settings of analysis parameters and output control. +# +# For detailed instructions on the context, see the default processing file: +# 20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini +# +# This version is modified to bilinear interpolation of measured points in output plots. +# + + +[Default] +# Whether to output detailed progress output, intermediate calculation results, etc. +verbose = True + +# Strings to denote this computation. Must be contiguous, with no space, tabs or line feeds. +measurement_id = 20250818_163443_SLTF-A_OLSLrsqw +post_process_id = p006_bilinear + +# Input +file_camera = Q:\Instruments\OLSL_OpticsLabSofastLandscape\camera_sofast_optics_lab_landscape_2025_02.h5 +file_orientation = Q:\Instruments\OLSL_OpticsLabSofastLandscape\spatial_orientation_optics_lab_landscape.h5 +file_display = Q:\Instruments\OLSL_OpticsLabSofastLandscape\OLSLrsqw_OpticsLabSofastLandscapeFringeSquareWhite\display_shape_optics_lab_landscape_square_distorted_3d_100x100.h5 +file_facet = Q:\Mirrors\SNLTF\0_Common\Facet_NSTTF.json +file_calibration = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A1_intensity_response\image_calibration_scaling_20250818_163358.h5 +file_measurement = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\A_Collect\A2_collection_result\20250818_163443_measurement_fringe.h5 + +# Directory to write output files. +# (The post_process_id below will be added as a suffix.) +dir_save_root = Q:\Mirrors\SNLTF\SNLTF-A\20250818_163443_OLSLrsqw\B_Post + +# Analysis control parameters +# Distance values are meters. +fit_initial_focal_length_x = 300.0 +fit_initial_focal_length_y = 300.0 +fit_robust_least_squares = True +fit_downsample = 10 + +# Measured mirror point interpolation +# As of this writing, surface choices are "nearest", "bilinear", and "clough_tocher" +# "nearest" the default, and will fill the plot domain. +# "bilinear" plots only show mirror points observed. +# "clough_tocher" piecewise cubic, curvature-minimizing interpolation. +#measured_interpolation_type = nearest +measured_interpolation_type = bilinear +#measured_interpolation_type = clough_tocher + +# Reference mirror +# As of this writing, surface choices are "symmetric_paraboloid", "astigmatic_paraboloid", and "plano". +# Distance values are meters. +reference_mirror_surface_type = symmetric_paraboloid +reference_mirror_focal_length_x = 100.0 +reference_mirror_focal_length_y = None +#reference_mirror_surface_type = astigmatic_paraboloid +#reference_mirror_focal_length_x = 100.0 +#reference_mirror_focal_length_y = 400.0 +#reference_mirror_surface_type = plano + +# Output control +# Output control flags are optional and may be omitted if brevity is desired. +# --------------------------------------------------------------------------- +output_fringe_images = True + +# Plot control parameters +# For detaled definitions and default values, see StandardPlotOutput.py, particularly the classes +# _OptionsFileOutput, _OptionsSlopeVis, _OptionsSlopeDeviationVis, _OptionsCurvatureVis, _OptionsRayTraceVis, and _RayTraceParameters. +# +# Plot control flags are optional and may be omitted if brevity is desired. +# ------------------------------------------------------------------------- +# File output control +plots.options_file_output.to_save = True +plots.options_file_output.save_dpi = 200 +plots.options_file_output.save_format = png +plots.options_file_output.close_after_save = False +plots.options_file_output.number_in_name = False + +# Slope plot control +plots.options_slope_vis.resolution = 0.001 +plots.options_slope_vis.clim = 7 +plots.options_slope_vis.quiver_density = 0.1 +plots.options_slope_vis.quiver_scale = 25 +plots.options_slope_vis.quiver_color = white +plots.options_slope_vis.to_plot = True + +# Slope deviation plot control +plots.options_slope_deviation_vis.resolution = 0.001 +plots.options_slope_deviation_vis.clim = 1.5 +plots.options_slope_deviation_vis.quiver_density = 0.1 +plots.options_slope_deviation_vis.quiver_scale = 25 +plots.options_slope_deviation_vis.quiver_color = white +plots.options_slope_deviation_vis.to_plot = True + +# Curvature plot control +plots.options_curvature_vis.resolution = 0.001 +plots.options_curvature_vis.clim = 50 +plots.options_curvature_vis.processing_1 = None +plots.options_curvature_vis.processing_2 = None +plots.options_curvature_vis.smooth_kernel_width = 1 +plots.options_curvature_vis.to_plot = True + +# Ray trace plot control +plots.options_ray_trace_vis.ray_trace_optic_res = 0.05 +plots.options_ray_trace_vis.hist_bin_res = 0.07 +plots.options_ray_trace_vis.hist_extent = 3 +plots.options_ray_trace_vis.enclosed_energy_max_semi_width = 1 +plots.options_ray_trace_vis.to_plot = True + +# Ray trace parameters +plots.options_ray_trace_vis.sun_direction_x = 0 +plots.options_ray_trace_vis.sun_direction_y = 0 +plots.options_ray_trace_vis.sun_direction_z = -1 +plots.options_ray_trace_vis.sun_sample_resolution = 40 + +plots.options_ray_trace_vis.v_target_center_x = 0 +plots.options_ray_trace_vis.v_target_center_y = 0 +plots.options_ray_trace_vis.v_target_center_z = 100 + +plots.options_ray_trace_vis.v_target_normal_x = 0 +plots.options_ray_trace_vis.v_target_normal_y = 0 +plots.options_ray_trace_vis.v_target_normal_z = -1 diff --git a/example/sofast_fringe/single_facet/example_process_single_facet.py b/example/sofast_fringe/single_facet/example_process_single_facet.py index fcc3a440a..57e7d317d 100644 --- a/example/sofast_fringe/single_facet/example_process_single_facet.py +++ b/example/sofast_fringe/single_facet/example_process_single_facet.py @@ -45,6 +45,7 @@ from opencsp.common.lib.camera.Camera import Camera from opencsp.common.lib.csp.LightSourceSun import LightSourceSun from opencsp.common.lib.csp.MirrorParametric import MirrorParametric, SYMMETRIC_PARABOLOID, ASTIGMATIC_PARABOLOID, PLANO +from opencsp.common.lib.csp.MirrorPoint import NEAREST_INTERPOLATION from opencsp.common.lib.csp.StandardPlotOutput import StandardPlotOutput from opencsp.common.lib.deflectometry.Surface2DParabolic import Surface2DParabolic from opencsp.common.lib.deflectometry.Surface2DPlano import Surface2DPlano @@ -74,6 +75,8 @@ def process_single_facet( fit_initial_focal_length_y: float, fit_robust_least_squares: bool, fit_downsample: int, + # Measured mirror surface + measured_interpolation_type: str, # Reference mirror surface reference_mirror_surface_type: str, reference_mirror_focal_length_x: float, @@ -196,7 +199,9 @@ def process_single_facet( ft.create_directories_if_necessary(dir_save_cur) # Get measured and reference optics - mirror_measured = sofast.get_optic().mirror.no_parent_copy() + # Measured mirror + mirror_measured = sofast.get_optic(measured_interpolation_type).mirror.no_parent_copy() + # Reference mirror if reference_mirror_surface_type == SYMMETRIC_PARABOLOID: mirror_reference = MirrorParametric.generate_symmetric_paraboloid( reference_mirror_focal_length_x, mirror_measured.region @@ -270,6 +275,9 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v fit_initial_focal_length_y = 300.0 fit_robust_least_squares = True fit_downsample = 10 + # Measured mirror surface + measured_interpolation_type = NEAREST_INTERPOLATION # Strings from MirrorPoint.py: GIVEN_INTERPOLATION, BILINEAR_INTERPOLATION, CLOUGH_TOCHER_INTERPOLATION, or NEAREST_INTERPOLATION + # Reference mirror surface reference_mirror_surface_type = SYMMETRIC_PARABOLOID # Strings from MirrorParametric.py: SYMMETRIC_PARABOLOID, ASTIGMATIC_PARABOLOID, or PLANO reference_mirror_focal_length_x = 100.0 # Ignored if plano @@ -328,6 +336,8 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v settings["Default"]["fit_robust_least_squares"] ) fit_downsample = int(settings["Default"]["fit_downsample"]) + # Measured mirror surface + measured_interpolation_type = str(settings["Default"]["measured_interpolation_type"]) # Reference mirror surface reference_mirror_surface_type = str(settings["Default"]["reference_mirror_surface_type"]) if reference_mirror_surface_type == PLANO: @@ -374,6 +384,7 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v lt.info('fit_initial_focal_length_y = ' + str(fit_initial_focal_length_y)) lt.info('fit_robust_least_squares = ' + str(fit_robust_least_squares)) lt.info('fit_downsample = ' + str(fit_downsample)) + lt.info('measured_interpolation_type = ' + str(measured_interpolation_type)) lt.info('reference_mirror_surface_type = ' + str(reference_mirror_surface_type)) lt.info('reference_mirror_focal_length_x = ' + str(reference_mirror_focal_length_x)) lt.info('reference_mirror_focal_length_y = ' + str(reference_mirror_focal_length_y)) @@ -400,6 +411,8 @@ def example_process_single_facet_driver(arg_settings_dir_body_ext: str = None, v fit_initial_focal_length_y, fit_robust_least_squares, fit_downsample, + # Measured mirror surface + measured_interpolation_type, # Reference mirror surface reference_mirror_surface_type, reference_mirror_focal_length_x, diff --git a/opencsp/common/lib/csp/MirrorPoint.py b/opencsp/common/lib/csp/MirrorPoint.py index 2bbacf106..7dcf0468b 100644 --- a/opencsp/common/lib/csp/MirrorPoint.py +++ b/opencsp/common/lib/csp/MirrorPoint.py @@ -16,6 +16,12 @@ from opencsp.common.lib.render.View3d import View3d from opencsp.common.lib.render_control.RenderControlMirror import RenderControlMirror +# Interpolation types +GIVEN_INTERPOLATION = "given" +BILINEAR_INTERPOLATION = "bilinear" +CLOUGH_TOCHER_INTERPOLATION = "clough_tocher" +NEAREST_INTERPOLATION = "nearest" + class MirrorPoint(MirrorAbstract): """ @@ -32,7 +38,8 @@ def __init__( surface_points: Pxyz, normal_vectors: Uxyz, shape: RegionXY, - interpolation_type: Literal["given", "bilinear", "clough_tocher", "nearest"] = "nearest", + # interpolation_type: Literal["given", "bilinear", "clough_tocher", "nearest"] = "nearest", + interpolation_type: str = NEAREST_INTERPOLATION, ) -> None: """ Initializes a MirrorPoint object with the specified surface points and normal vectors. @@ -88,7 +95,7 @@ def _define_interpolation( If given interpolation type is not supported. """ # Interpolate - if interpolation_type == "bilinear": + if interpolation_type == BILINEAR_INTERPOLATION: # Z coordinate interpolation object points_xy = self.surface_points.projXY().data.T # Nx2 array Z = self.surface_points.z @@ -96,7 +103,7 @@ def _define_interpolation( # Normal vector interpolation object Z_N = self.normal_vectors.data.T self.normals_function = interp.LinearNDInterpolator(points_xy, Z_N, np.nan) - elif interpolation_type == "clough_tocher": + elif interpolation_type == CLOUGH_TOCHER_INTERPOLATION: # Z coordinate interpolation object points_xy = self.surface_points.projXY().data.T # Nx2 array Z = self.surface_points.z @@ -104,7 +111,7 @@ def _define_interpolation( # Normal vector interpolation object Z_N = self.normal_vectors.data.T self.normals_function = interp.CloughTocher2DInterpolator(points_xy, Z_N, np.nan) - elif interpolation_type == "nearest": + elif interpolation_type == NEAREST_INTERPOLATION: # Z coordinate interpolation object points_xy = self.surface_points.projXY().data.T # Nx2 array Z = self.surface_points.z @@ -112,7 +119,7 @@ def _define_interpolation( # Normal vector interpolatin object Z_N = self.normal_vectors.data.T self.normals_function = interp.NearestNDInterpolator(points_xy, Z_N) - elif interpolation_type == "given": + elif interpolation_type == GIVEN_INTERPOLATION: # Z coordinate lookup function points_lookup = { (x, y): z for x, y, z in zip(self.surface_points.x, self.surface_points.y, self.surface_points.z)