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() diff --git a/example/conftest.py b/example/conftest.py index 0dfb0fd92..0fc64dac0 100644 --- a/example/conftest.py +++ b/example/conftest.py @@ -1,19 +1,31 @@ +""" +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('--verbose', action='store', default='False', help='Output detailed information.') @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 verbose_fixture(request): +# return request.config.getoption('--verbose') 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..43978058b 100644 --- a/example/scene_reconstruction/example_scene_reconstruction.py +++ b/example/scene_reconstruction/example_scene_reconstruction.py @@ -1,5 +1,78 @@ -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 + +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 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: + 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. + +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. + Make a scratch copy somewhere else, edit in VS Code, then press F5 key. + +""" + +from os.path import join, basename, dirname, splitext + +import argparse +import configparser import numpy as np from opencsp.app.scene_reconstruction.lib.SceneReconstruction import SceneReconstruction @@ -10,14 +83,12 @@ import opencsp.common.lib.tool.log_tools as lt -def scene_reconstruction(dir_output, dir_input): +def scene_reconstruction(dir_input, dir_output, verbose): """ 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 +97,10 @@ 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. + verbose : bool + If true, write out detailed information. Notes ----- @@ -42,7 +117,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,26 +149,105 @@ 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') + # 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(arg_settings_dir_body_ext: str = None, verbose_param=None): + """ + Sets up and runs the scene_reconstruction() routine. - 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: - dir_input = dir_input_fixture - if dir_output_fixture: - dir_output = dir_output_fixture + Parameters + ---------- - # Define output directory - ft.create_directories_if_necessary(dir_input) + 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. - # Set up logger - lt.logger(join(dir_output, 'log.txt'), lt.log.INFO) + 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.") + 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) - scene_reconstruction(dir_output, dir_input) + # Set up logger + 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('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__': - example_driver() + # 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='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( + "-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_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_main, verbose_main) 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..81c5943a3 --- /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: + 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..7d250490e --- /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] +# 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 write output files. +dir_output = C:\ctemp\OpenCSP_ctemp\example_data_large\scene_reconstruction\example_scene_reconstruction_output diff --git a/example/sofast_fringe/example_process_single_facet.py b/example/sofast_fringe/example_process_single_facet.py deleted file mode 100644 index af1c3cdb5..000000000 --- a/example/sofast_fringe/example_process_single_facet.py +++ /dev/null @@ -1,213 +0,0 @@ -"""Module for processing and analyzing SOFAST data for a single facet mirror. - -This script performs the following steps: -1. Load saved single facet SOFAST collection data from an HDF5 file. -2. Save projected sinusoidal fringe images to PNG format. -3. Save captured sinusoidal fringe images and mask images to PNG format. -4. Process data with SOFAST and save processed data to HDF5. -5. Generate a suite of plots and save image files. - -Examples --------- -To run the script, simply execute it as a standalone program: - ->>> python example_process_single_facet.py - -This will perform the processing steps and save the results to the data/output/single_facet directory -with the following subfolders: -1_images_fringes_projected - The patterns sent to the display during the SOFAST measurement of the optic. -2_images_captured - The captured images of the displayed patterns as seen by the SOFAST camera -3_processed_data - The processed data from SOFAST. -4_processed_output_figures - The output figure suite from a SOFAST characterization. - -Notes ------ -- The script assumes that the input data files are located in the specified directories. -- Chat GPT 40 assisted with the generation of some docstrings in this file. -""" - -import json -from os.path import join, dirname - -import imageio.v3 as imageio - -from opencsp.app.sofast.lib.DisplayShape import DisplayShape as Display -from opencsp.app.sofast.lib.DefinitionFacet import DefinitionFacet -from opencsp.app.sofast.lib.Fringes import Fringes -from opencsp.app.sofast.lib.ImageCalibrationScaling import ImageCalibrationScaling -from opencsp.app.sofast.lib.MeasurementSofastFringe import MeasurementSofastFringe -from opencsp.app.sofast.lib.ProcessSofastFringe import ProcessSofastFringe as Sofast -from opencsp.app.sofast.lib.SofastConfiguration import SofastConfiguration -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.StandardPlotOutput import StandardPlotOutput -from opencsp.common.lib.deflectometry.Surface2DParabolic import Surface2DParabolic -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 -import opencsp.common.lib.tool.file_tools as ft -import opencsp.common.lib.tool.log_tools as lt - - -def example_process_single_facet(): - """Performs processing of previously collected SOFAST data of single facet mirror. - - 1. Load saved single facet SOFAST collection data from HDF5 file - 2. Save projected sinusoidal fringe images to PNG format - 3. Save captured sinusoidal fringe images and mask images to PNG format - 4. Processes data with SOFAST and save processed data to HDF5 - 5. Generate plot suite and save image files - """ - # General setup - # ============= - - # Define save dir - dir_save = join(dirname(__file__), "data/output/single_facet") - 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) - display = Display.load_from_hdf(file_display) - orientation = SpatialOrientation.load_from_hdf(file_orientation) - measurement = MeasurementSofastFringe.load_from_hdf(file_measurement) - 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, "1_images_fringes_projected") - 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, 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) - - # 3. Save captured sinusoidal fringe images and mask images to PNG format - # ======================================================================= - dir_save_cur = join(dir_save, "2_images_captured") - 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, 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) - # 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) - - # 4. Processes data with Sofast and save processed data to HDF5 - # ============================================================= - dir_save_cur = join(dir_save, "3_processed_data") - 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) - - # Calibrate fringes - (aka sinosoidal image) - measurement.calibrate_fringe_images(calibration) - - # Instantiate sofast object - sofast = Sofast(measurement, orientation, camera, display) - - # 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 - config = SofastConfiguration() - config.load_sofast_object(sofast) - measurement_stats = config.get_measurement_stats() - - # Save measurement stats as JSON - with open(join(dir_save_cur, "measurement_statistics.json"), "w", encoding="utf-8") as f: - json.dump(measurement_stats, f, indent=3) - - # 5. Generate plot suite and save images files - # ============================================ - dir_save_cur = join(dir_save, "4_processed_output_figures") - ft.create_directories_if_necessary(dir_save_cur) - - # Get measured and reference optics - 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() - 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 - - # Create standard output plots - plots.plot() - - -if __name__ == "__main__": - example_process_single_facet() 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..33184d29f --- /dev/null +++ b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p001_default_process_single_facet_settings_Q.ini @@ -0,0 +1,227 @@ +# 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 +# +# 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\ +# 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 +# +# 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\ +# 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 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. +# + + +[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 = 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 +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/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..c50f9d16e --- /dev/null +++ b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p002_fast_process_single_facet_settings_Q.ini @@ -0,0 +1,121 @@ +# 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 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 + +# 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 +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 = 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 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/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 new file mode 100644 index 000000000..d74c5716b --- /dev/null +++ b/example/sofast_fringe/single_facet/20250818_163443_SNLTF-A_OLSLrsqw_p003_refx25m_refy25m_process_single_facet_settings_Q.ini @@ -0,0 +1,121 @@ +# 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_refx25m_refy25m + +# 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 = 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. +# --------------------------------------------------------------------------- +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..51f089f46 --- /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,121 @@ +# 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 + +# 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/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..63eb459c6 --- /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,121 @@ +# 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 + +# 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/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 new file mode 100644 index 000000000..57e7d317d --- /dev/null +++ b/example/sofast_fringe/single_facet/example_process_single_facet.py @@ -0,0 +1,461 @@ +"""Module for processing and analyzing SOFAST data for a single facet mirror. + +This script performs the following steps: +1. Load saved single facet SOFAST collection data from an HDF5 file. +2. Save projected sinusoidal fringe images to PNG format. +3. Save captured sinusoidal fringe images and mask images to PNG format. +4. Process data with SOFAST and save processed data to HDF5. +5. Generate a suite of plots and save image files. + +Examples +-------- +To run the script, simply execute it as a standalone program: + +>>> python example_process_single_facet.py + +This will perform the processing steps and save the results to the data/output/single_facet directory +with the following subfolders: +1_images_fringes_projected - The patterns sent to the display during the SOFAST measurement of the optic. +2_images_captured - The captured images of the displayed patterns as seen by the SOFAST camera +3_processed_data - The processed data from SOFAST. +4_processed_output_figures - The output figure suite from a SOFAST characterization. + +Notes +----- +- The script assumes that the input data files are located in the specified directories. +- Chat GPT 40 assisted with the generation of some docstrings in this file. +""" + +import json +from os.path import join, basename, dirname, splitext + +import argparse +import configparser + +import imageio.v3 as imageio + +from opencsp.app.sofast.lib.DisplayShape import DisplayShape as Display +from opencsp.app.sofast.lib.DefinitionFacet import DefinitionFacet +from opencsp.app.sofast.lib.Fringes import Fringes +from opencsp.app.sofast.lib.ImageCalibrationScaling import ImageCalibrationScaling +from opencsp.app.sofast.lib.MeasurementSofastFringe import MeasurementSofastFringe +from opencsp.app.sofast.lib.ProcessSofastFringe import ProcessSofastFringe as Sofast +from opencsp.app.sofast.lib.SofastConfiguration import SofastConfiguration +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, 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 +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 +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( + 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, + # Measured mirror surface + measured_interpolation_type: str, + # Reference mirror surface + reference_mirror_surface_type: str, + reference_mirror_focal_length_x: float, + reference_mirror_focal_length_y: float, + # Output rendering control + output_fringe_images: bool, + plots: StandardPlotOutput, +): + """Performs processing of previously collected SOFAST data of single facet mirror. + + 1. Load saved single facet SOFAST collection data from HDF5 file + 2. Save projected sinusoidal fringe images to PNG format + 3. Save captured sinusoidal fringe images and mask images to PNG format + 4. Processes data with SOFAST and save processed data to HDF5 + 5. Generate plot suite and save image files + """ + # General setup + # ============= + + # Set up save dir + ft.create_directories_if_necessary(dir_save) + + # Construct output file prefix + output_file_prefix = measurement_id + "_" + post_process_id + "_" + + # 1. Load saved single facet Sofast collection data + # ================================================= + camera = Camera.load_from_hdf(file_camera) + display = Display.load_from_hdf(file_display) + orientation = SpatialOrientation.load_from_hdf(file_orientation) + measurement = MeasurementSofastFringe.load_from_hdf(file_measurement) + calibration = ImageCalibrationScaling.load_from_hdf(file_calibration) + facet_data = DefinitionFacet.load_from_json(file_facet) + + 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 + # ============================================================= + 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 + 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, + 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 ["{SYMMETRIC_PARABOLOID}", "{ASTIGMATIC_PARABOLOID}, "{PLANO}"].', + ) + fit_surface = None # Eliminate Pylint error message. Never executes. + + # Calibrate fringes - (aka sinosoidal image) + measurement.calibrate_fringe_images(calibration) + + # Instantiate sofast object + sofast = Sofast(measurement, orientation, camera, display) + + # Process + sofast.process_optic_singlefacet(facet_data, fit_surface) + + # 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, 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 + # ============================================ + dir_save_cur = join(dir_save, "B4_output_figures") + ft.create_directories_if_necessary(dir_save_cur) + + # Get measured and reference optics + # 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 + ) + 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 ["{SYMMETRIC_PARABOLOID}", "{ASTIGMATIC_PARABOLOID}, "{PLANO}"].', + ) + mirror_reference = None # Eliminate Pylint error message. Never executes. + + # Save optic objects and output destination + plots.optic_measured = mirror_measured + plots.optic_reference = mirror_reference + plots.options_file_output.output_dir = dir_save_cur + plots.options_file_output.file_prefix = output_file_prefix + + # Create standard output plots + 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. + """ + # Setup plot control, which 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.") + # 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") + # 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 + # 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 + 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. + 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) + 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 + # 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"] + 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_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"]) + # 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: + reference_mirror_focal_length_x = None + reference_mirror_focal_length_y = None + else: + 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"]) + else: + output_fringe_images = True + # Set plot control parameters + plots.set_plot_control_from_settings(settings) + + # Ensure output directory is ready + ft.create_directories_if_necessary(dir_save) + + # Set up logger + 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 + 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)) + 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('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)) + 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, + # Measured mirror surface + measured_interpolation_type, + # Reference mirror surface + reference_mirror_surface_type, + reference_mirror_focal_length_x, + reference_mirror_focal_length_y, + # Output rendering control + output_fringe_images, + plots, + ) + + +if __name__ == "__main__": + # 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_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_main, verbose_main) 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 new file mode 100644 index 000000000..5899b8e62 --- /dev/null +++ b/example/sofast_fringe/single_facet/example_process_single_facet_README.md @@ -0,0 +1,163 @@ +example_process_single_facet_README.txt: +======================================== +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 +---------- + +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\single_facet\example_process_single_facet.py + + +Default Run on Built-In Data +---------------------------- + +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. + +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 +----------------- + +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. 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 +----------------- + +For an example settings file designed for exploration, see: + + + + 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: 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 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 +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:' + 1. Create a directory "C:\ctemp\OpenCSP_ctemp\example_data_large" for holding example data. + 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_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_process_single_facet.py". + 7. Run the script, providing the -s option and pointing to the .ini file: + 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_process_single_facet_output" subdirectory + 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 + "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_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_process_single_facet.py". + b. Run the script, providing the -s option and pointing to your .ini file: + 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: + 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/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 diff --git a/opencsp/common/lib/csp/MirrorParametric.py b/opencsp/common/lib/csp/MirrorParametric.py index 68da747d6..255a736e6 100644 --- a/opencsp/common/lib/csp/MirrorParametric.py +++ b/opencsp/common/lib/csp/MirrorParametric.py @@ -17,6 +17,11 @@ from opencsp.common.lib.geometry.Vxy import Vxy from opencsp.common.lib.geometry.FunctionXYContinuous import FunctionXYContinuous +# Mirror surface types +SYMMETRIC_PARABOLOID = "symmetric_paraboloid" +ASTIGMATIC_PARABOLOID = "astigmatic_paraboloid" +PLANO = "plano" + class MirrorParametric(MirrorAbstract): """ @@ -144,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/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) diff --git a/opencsp/common/lib/csp/StandardPlotOutput.py b/opencsp/common/lib/csp/StandardPlotOutput.py index 38616b1cb..e467ef02c 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,25 @@ 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 +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 = '' + """String to prefix each output file, including separator""" @dataclass @@ -76,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(). @@ -100,22 +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)""" - - @dataclass class _RayTraceParameters: source = LightSourceSun.from_given_sun_position(Uxyz((0, 0, -1)), resolution=20) @@ -183,6 +186,182 @@ 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. + # 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. + # 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. + # 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) + 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)) + def plot(self): """Creates standard output plot suite""" # This function checks if plotting is turned on @@ -254,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 @@ -270,7 +451,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 +476,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 +501,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 +547,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 +613,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 +636,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 +659,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 +688,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 +712,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 +736,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 +761,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( 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) diff --git a/opencsp/common/lib/tool/log_tools.py b/opencsp/common/lib/tool/log_tools.py index cb8791283..f614f4a17 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:: @@ -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()) diff --git a/opencsp/common/lib/tool/string_tools.py b/opencsp/common/lib/tool/string_tools.py index 15a1ffd37..d158702a8 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,76 @@ 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 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. + 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