diff --git a/contrib/test_data_generation/sofast_fringe/generate_test_data_multi_facet.py b/contrib/test_data_generation/sofast_fringe/generate_test_data_multi_facet.py deleted file mode 100644 index d2acc6346..000000000 --- a/contrib/test_data_generation/sofast_fringe/generate_test_data_multi_facet.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Generates test data from measurement file for mirror type 'multi_facet'. -""" - -from os.path import join, dirname, exists - -import matplotlib.pyplot as plt -import numpy as np - -from opencsp.app.sofast.lib.ImageCalibrationScaling import ImageCalibrationScaling -from opencsp.app.sofast.lib.MeasurementSofastFringe import MeasurementSofastFringe as Measurement -from opencsp.app.sofast.lib.ProcessSofastFringe import ProcessSofastFringe as Sofast -from opencsp.app.sofast.lib.DisplayShape import DisplayShape as Display -from opencsp.app.sofast.lib.DefinitionEnsemble import DefinitionEnsemble -from opencsp.app.sofast.lib.DefinitionFacet import DefinitionFacet -from opencsp.common.lib.camera.Camera import Camera -from opencsp.common.lib.opencsp_path.opencsp_root_path import opencsp_code_dir - - -def generate_dataset( - file_measurement: str, - file_camera: str, - file_display: str, - file_calibration: str, - file_facet: str, - file_ensemble: str, - file_dataset_out: str, -): - """Generates and saves test data""" - # Check output file exists - if not exists(dirname(file_dataset_out)): - raise FileNotFoundError(f'Output directory {file_dataset_out:s} does not exist.') - - # Load components - camera = Camera.load_from_hdf(file_camera) - display = Display.load_from_hdf(file_display) - measurement = Measurement.load_from_hdf(file_measurement) - calibration = ImageCalibrationScaling.load_from_hdf(file_calibration) - ensemble_data = DefinitionEnsemble.load_from_json(file_ensemble) - facet_data = [DefinitionFacet.load_from_json(file_facet)] * ensemble_data.num_facets - - # Calibrate fringes - measurement.calibrate_fringe_images(calibration) - - # Create sofast object - sofast = Sofast(measurement, camera, display) - - # Update image processing parameters - sofast.params.mask_hist_thresh = 0.83 - sofast.params.geometry_params.perimeter_refine_perpendicular_search_dist = 10.0 - sofast.params.geometry_params.facet_corns_refine_frac_keep = 1.0 - sofast.params.geometry_params.facet_corns_refine_perpendicular_search_dist = 3.0 - sofast.params.geometry_params.facet_corns_refine_step_length = 5.0 - - # Define surface data - surface_data = [ - dict( - surface_type='parabolic', initial_focal_lengths_xy=(100.0, 100.0), robust_least_squares=False, downsample=10 - ) - ] * ensemble_data.num_facets - - # Process optic data - sofast.process_optic_multifacet(facet_data, ensemble_data, surface_data) - - # Save data - sofast.save_to_hdf(file_dataset_out) - display.save_to_hdf(file_dataset_out) - camera.save_to_hdf(file_dataset_out) - calibration.save_to_hdf(file_dataset_out) - print(f'Data saved to: {file_dataset_out:s}') - - # Show slope map - mask = sofast.data_image_processing_general.mask_raw - image = np.zeros(mask.shape) * np.nan - for idx in range(sofast.num_facets): - mask = sofast.data_image_processing_facet[idx].mask_processed - slopes_xy = sofast.data_calculation_facet[idx].slopes_facet_xy - slopes = np.sqrt(np.sum(slopes_xy**2, 0)) - image[mask] = slopes - plt.figure() - plt.imshow(image, cmap='jet') - plt.title('Slope Magnitude') - plt.show() - - -if __name__ == '__main__': - # Generate measurement set 1 - base_dir = join(opencsp_code_dir(), 'test/data/measurements_sofast_fringe') - - generate_dataset( - file_measurement=join(base_dir, 'measurement_ensemble.h5'), - file_camera=join(base_dir, 'camera.h5'), - file_display=join(base_dir, 'display_distorted_2d.h5'), - file_calibration=join(base_dir, 'image_calibration.h5'), - file_facet=join(base_dir, 'Facet_lab_6x4.json'), - file_ensemble=join(base_dir, 'Ensemble_lab_6x4.json'), - file_dataset_out=join(base_dir, 'calculations_facet_ensemble/data.h5'), - ) diff --git a/example/csp/example_optics_and_ray_tracing.py b/example/csp/example_optics_and_ray_tracing.py index 428c4be61..27ae1c855 100644 --- a/example/csp/example_optics_and_ray_tracing.py +++ b/example/csp/example_optics_and_ray_tracing.py @@ -201,7 +201,7 @@ def define_mirror_array(focal_length: float) -> FacetEnsemble: # Build facet ensemble facet_ensemble = FacetEnsemble(facets) facet_ensemble.set_facet_positions(facet_locations) - facet_ensemble.set_facet_canting(facet_canting) + facet_ensemble.set_facet_cantings(facet_canting) return facet_ensemble # FacetEnsemble.generate_rotation_defined(facets) diff --git a/example/mirror/example_MirrorOutput.py b/example/mirror/example_MirrorOutput.py index 99c16c1cc..0f04c7ec8 100644 --- a/example/mirror/example_MirrorOutput.py +++ b/example/mirror/example_MirrorOutput.py @@ -132,7 +132,7 @@ def setUp(self): tilt_left * tilt_down, tilt_right * tilt_down, ] - fe2x2.set_facet_canting(fe_2x2_canting_rotations) + fe2x2.set_facet_cantings(fe_2x2_canting_rotations) self.h2x2 = HeliostatAzEl(fe2x2, name='Simple 2x2 Heliostat') self.h2x2_title = 'Heliostat with Parametrically Defined Facets' diff --git a/example/raytrace/example_RayTraceOutput.py b/example/raytrace/example_RayTraceOutput.py index 71b0a1de0..ef59c8ff3 100644 --- a/example/raytrace/example_RayTraceOutput.py +++ b/example/raytrace/example_RayTraceOutput.py @@ -131,7 +131,7 @@ def setUp(self): self.fe2x2 = FacetEnsemble(self.h2x2_facets) fe2x2_positions = Pxyz([[-1.1, 1.1, -1.1, 1.1], [1.6, 1.6, -1.6, -1.6], [0, 0, 0, 0]]) self.fe2x2.set_facet_positions(fe2x2_positions) - self.fe2x2.set_facet_canting(self.h2x2_canting) + self.fe2x2.set_facet_cantings(self.h2x2_canting) self.h2x2 = HeliostatAzEl(self.fe2x2, 'Simple 2x2 Heliostat') self.h2x2.pivot = 0 self.h2x2_title = 'Heliostat with Parametrically Defined Facets' diff --git a/example/sofast_fringe/example_process_facet_ensemble.py b/example/sofast_fringe/example_process_facet_ensemble.py index 4cf83c4f3..8bf5b253a 100644 --- a/example/sofast_fringe/example_process_facet_ensemble.py +++ b/example/sofast_fringe/example_process_facet_ensemble.py @@ -1,40 +1,68 @@ +"""Module for processing and analyzing SOFAST data for an ensemble of mirrors. + +This script performs the following steps: + +1. Load saved facet ensemble 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_facet_ensemble.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 4o assisted with the generation of some docstrings in this file.""" + +import json from os.path import join, dirname -from scipy.spatial.transform import Rotation + +import imageio.v3 as imageio from opencsp.app.sofast.lib.DisplayShape import DisplayShape from opencsp.app.sofast.lib.DefinitionEnsemble import DefinitionEnsemble 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 -from opencsp.app.sofast.lib.SpatialOrientation import SpatialOrientation 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.FacetEnsemble import FacetEnsemble +from opencsp.common.lib.csp.Facet import Facet +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.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.render_control.RenderControlMirror as rcm -import opencsp.common.lib.render_control.RenderControlFacet as rcf -import opencsp.common.lib.render_control.RenderControlFacetEnsemble as rcfe import opencsp.common.lib.tool.file_tools as ft import opencsp.common.lib.tool.log_tools as lt -from opencsp.common.lib.geometry.Vxyz import Vxyz -from opencsp.common.lib.tool.hdf5_tools import load_hdf5_datasets def example_process_facet_ensemble(): - """Performs processing of previously collected Sofast data - of multi facet mirror ensemble: + """Performs processing of previously collected SOFAST data of facet ensemble. 1. Load saved facet ensemble Sofast collection data - 2. Processes data with Sofast - 3. Log best-fit parabolic focal lengths - 4. Plot slope magnitude - 5. Plot 3d representation of facet ensemble - 6. Save slope data as 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 images files """ # General setup # ============= @@ -44,35 +72,70 @@ def example_process_facet_ensemble(): ft.create_directories_if_necessary(dir_save) # Set up logger - lt.logger(join(dir_save, 'log.txt'), lt.log.INFO) + lt.logger(join(dir_save, 'log.txt'), lt.log.WARN) - base_dir = join(opencsp_code_dir(), 'test/data/sofast_fringe') + # 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_dataset = join(base_dir, 'data_expected_facet_ensemble/data.h5') - file_measurement = join(base_dir, 'data_measurement/measurement_ensemble.h5') - - # Load data - camera = Camera.load_from_hdf(file_dataset) - display = DisplayShape.load_from_hdf(file_dataset) - orientation = SpatialOrientation.load_from_hdf(file_dataset) + file_measurement = join(dir_data_sofast, 'data_measurement/measurement_ensemble.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_lab_6x4.json') + file_ensemble = join(dir_data_common, 'Ensemble_lab_6x4.json') + file_params = join(dir_data_sofast, 'data_expected_facet_ensemble/data.h5') + + # 1. Load saved facet ensemble Sofast collection data + # ================================================= + camera = Camera.load_from_hdf(file_camera) + display = DisplayShape.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_dataset) - - # Load sofast params - datasets = [ - 'DataSofastInput/sofast_params/mask_hist_thresh', - 'DataSofastInput/sofast_params/mask_filt_width', - 'DataSofastInput/sofast_params/mask_filt_thresh', - 'DataSofastInput/sofast_params/mask_thresh_active_pixels', - 'DataSofastInput/sofast_params/mask_keep_largest_area', - 'DataSofastInput/sofast_params/perimeter_refine_axial_search_dist', - 'DataSofastInput/sofast_params/perimeter_refine_perpendicular_search_dist', - 'DataSofastInput/sofast_params/facet_corns_refine_step_length', - 'DataSofastInput/sofast_params/facet_corns_refine_perpendicular_search_dist', - 'DataSofastInput/sofast_params/facet_corns_refine_frac_keep', - ] - params = load_hdf5_datasets(datasets, file_dataset) + calibration = ImageCalibrationScaling.load_from_hdf(file_calibration) + data_ensemble = DefinitionEnsemble.load_from_json(file_ensemble) + data_facets = [DefinitionFacet.load_from_json(file_facet)] * data_ensemble.num_facets + data_surfaces = [Surface2DParabolic((500.0, 500.0), False, 10)] * data_ensemble.num_facets + + # 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]) + 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 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 + 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 + 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) # Calibrate measurement measurement.calibrate_fringe_images(calibration) @@ -80,103 +143,61 @@ def example_process_facet_ensemble(): # Instantiate sofast object sofast = ProcessSofastFringe(measurement, orientation, camera, display) - # Update parameters - sofast.params.mask.hist_thresh = params['mask_hist_thresh'] - sofast.params.mask.filt_width = params['mask_filt_width'] - sofast.params.mask.filt_thresh = params['mask_filt_thresh'] - sofast.params.mask.thresh_active_pixels = params['mask_thresh_active_pixels'] - sofast.params.mask.keep_largest_area = params['mask_keep_largest_area'] - - sofast.params.geometry.perimeter_refine_axial_search_dist = params['perimeter_refine_axial_search_dist'] - sofast.params.geometry.perimeter_refine_perpendicular_search_dist = params[ - 'perimeter_refine_perpendicular_search_dist' - ] - sofast.params.geometry.facet_corns_refine_step_length = params['facet_corns_refine_step_length'] - sofast.params.geometry.facet_corns_refine_perpendicular_search_dist = params[ - 'facet_corns_refine_perpendicular_search_dist' - ] - sofast.params.geometry.facet_corns_refine_frac_keep = params['facet_corns_refine_frac_keep'] - - # Load ensemble data - datasets = [ - 'DataSofastInput/optic_definition/ensemble/ensemble_perimeter', - 'DataSofastInput/optic_definition/ensemble/r_facet_ensemble', - 'DataSofastInput/optic_definition/ensemble/v_centroid_ensemble', - 'DataSofastInput/optic_definition/ensemble/v_facet_locations', - ] - ensemble_data = load_hdf5_datasets(datasets, file_dataset) - ensemble_data = DefinitionEnsemble( - Vxyz(ensemble_data['v_facet_locations']), - [Rotation.from_rotvec(r) for r in ensemble_data['r_facet_ensemble']], - ensemble_data['ensemble_perimeter'], - Vxyz(ensemble_data['v_centroid_ensemble']), - ) - - facet_data = [] - for idx in range(len(ensemble_data.r_facet_ensemble)): - datasets = [ - f'DataSofastInput/optic_definition/facet_{idx:03d}/v_centroid_facet', - f'DataSofastInput/optic_definition/facet_{idx:03d}/v_facet_corners', - ] - data = load_hdf5_datasets(datasets, file_dataset) - facet_data.append(DefinitionFacet(Vxyz(data['v_facet_corners']), Vxyz(data['v_centroid_facet']))) - - # Load surface data - surfaces = [] - for idx in range(len(facet_data)): - datasets = [ - f'DataSofastInput/surface_params/facet_{idx:03d}/downsample', - f'DataSofastInput/surface_params/facet_{idx:03d}/initial_focal_lengths_xy', - f'DataSofastInput/surface_params/facet_{idx:03d}/robust_least_squares', - ] - data = load_hdf5_datasets(datasets, file_dataset) - data['robust_least_squares'] = bool(data['robust_least_squares']) - surfaces.append(Surface2DParabolic(**data)) + # Update sofast processing parameters + sofast.params = sofast.params.load_from_hdf(file_params, 'DataSofastInput/') # Process - sofast.process_optic_multifacet(facet_data, ensemble_data, surfaces) - - # 3. Log best-fit parabolic focal lengths - # ======================================= - sofast_config = SofastConfiguration() - sofast_config.load_sofast_object(sofast) - sofast_stats = sofast_config.get_measurement_stats() - for stat in sofast_stats: - focal_lengths_xy = stat['focal_lengths_parabolic_xy'] - lt.info(f'Facet {idx:d} xy focal lengths (meters): {focal_lengths_xy[0]:.3f}, {focal_lengths_xy[1]:.3f}') - - # 4. Plot slope magnitude - # ======================= - - # Get optic representation - ensemble: FacetEnsemble = sofast.get_optic() - - # Generate plots - figure_control = rcfg.RenderControlFigure(tile_array=(1, 1), tile_square=True) - mirror_control = rcm.RenderControlMirror(centroid=True, surface_normals=True, norm_res=1) - facet_control = rcf.RenderControlFacet( - draw_mirror_curvature=True, mirror_styles=mirror_control, draw_outline=False, draw_surface_normal=True - ) - facet_ensemble_control = rcfe.RenderControlFacetEnsemble(default_style=facet_control, draw_outline=True) - axis_control_m = rca.meters() - - # Plot slope map - res = 0.002 # meter, make the plot with 2mm spatial resolution - clim = 7 # mrad, draw the plot with +/-7mrad scale bars, this mirror has erorrs that extend to about +/-7mrad - fig_record = fm.setup_figure(figure_control, axis_control_m, title='') - ensemble.plot_orthorectified_slope(res=res, clim=clim, axis=fig_record.axis) - fig_record.save(dir_save, 'slope_magnitude', 'png') - - # 5. Plot 3d representation of facet ensemble - # =========================================== - fig_record = fm.setup_figure_for_3d_data(figure_control, axis_control_m, title='Facet Ensemble') - ensemble.draw(fig_record.view, facet_ensemble_control) - fig_record.axis.axis('equal') - fig_record.save(dir_save, 'facet_ensemble', 'png') - - # 6. Save slope data as HDF5 file - # =============================== - sofast.save_to_hdf(f'{dir_save}/data_multifacet.h5') + sofast.process_optic_multifacet(data_facets, data_ensemble, data_surfaces) + + # 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 + ensemble_measured = sofast.get_optic() + region = ensemble_measured.facets[0].mirror.region + facets = [Facet(MirrorParametric.generate_flat(region)) for _ in range(sofast.num_facets)] + ensemble_reference = FacetEnsemble(facets) + ensemble_reference.set_facet_positions(data_ensemble.v_facet_locations) + + # 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 = ensemble_measured + plots.optic_reference = ensemble_reference + + # Update visualization parameters + plots.options_slope_vis.clim = 7 + plots.options_slope_deviation_vis.clim = 1.5 + 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__': diff --git a/example/sofast_fringe/example_process_single_facet.py b/example/sofast_fringe/example_process_single_facet.py index a2772487a..a94bd8eec 100644 --- a/example/sofast_fringe/example_process_single_facet.py +++ b/example/sofast_fringe/example_process_single_facet.py @@ -1,30 +1,64 @@ +"""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.Facet import Facet +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.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.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. + """Performs processing of previously collected SOFAST data of single facet mirror. - 1. Load saved single facet Sofast collection data - 2. Processes data with Sofast - 3. Log best-fit parabolic focal lengths - 4. Plot slope magnitude - 5. Save slope data as HDF5 file + 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 images files """ # General setup # ============= @@ -34,7 +68,7 @@ def example_process_single_facet(): ft.create_directories_if_necessary(dir_save) # Set up logger - lt.logger(join(dir_save, 'log.txt'), lt.log.INFO) + 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') @@ -48,6 +82,19 @@ def example_process_single_facet(): 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) @@ -57,8 +104,43 @@ def example_process_single_facet(): calibration = ImageCalibrationScaling.load_from_hdf(file_calibration) facet_data = DefinitionFacet.load_from_json(file_facet) - # 2. Process data with Sofast - # =========================== + # 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]) + 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 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 + 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 + 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) surface = Surface2DParabolic(initial_focal_lengths_xy=(300.0, 300.0), robust_least_squares=True, downsample=10) @@ -72,30 +154,59 @@ def example_process_single_facet(): # Process sofast.process_optic_singlefacet(facet_data, surface) - # 3. Log best-fit parabolic focal lengths - # ======================================= - surf_coefs = sofast.data_calculation_facet[0].surf_coefs_facet - focal_lengths_xy = [1 / 4 / surf_coefs[2], 1 / 4 / surf_coefs[5]] - lt.info(f'Facet xy focal lengths (meters): ' f'{focal_lengths_xy[0]:.3f}, {focal_lengths_xy[1]:.3f}') + # 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 - # 4. Plot slope magnitude - # ======================= + plots.options_curvature_vis.resolution = 0.001 - # Get optic representation - facet: Facet = sofast.get_optic() + plots.options_ray_trace_vis.enclosed_energy_max_semi_width = 1 - # Generate plots - figure_control = rcfg.RenderControlFigure(tile_array=(1, 1), tile_square=True) - axis_control_m = rca.meters() + plots.options_file_output.to_save = True + plots.options_file_output.number_in_name = False + plots.options_file_output.output_dir = dir_save_cur - # Plot slope map - fig_record = fm.setup_figure(figure_control, axis_control_m, title='') - facet.plot_orthorectified_slope(res=0.002, clim=7, axis=fig_record.axis) - fig_record.save(dir_save, 'slope_magnitude', 'png') + # 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 - # 5. Save slope data as HDF5 file - # =============================== - sofast.save_to_hdf(f'{dir_save}/data_singlefacet.h5') + # Create standard output plots + plots.plot() if __name__ == '__main__': diff --git a/example/sofast_fringe/example_process_undefined_shape.py b/example/sofast_fringe/example_process_undefined_shape.py index 45efa07f0..1c585fcc4 100644 --- a/example/sofast_fringe/example_process_undefined_shape.py +++ b/example/sofast_fringe/example_process_undefined_shape.py @@ -1,29 +1,65 @@ +"""Module for processing and analyzing SOFAST data for a single facet mirror of unknown shape. + +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_undefined_shape.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 json +import imageio.v3 as imageio + +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.DisplayShape import DisplayShape as Display from opencsp.app.sofast.lib.SpatialOrientation import SpatialOrientation from opencsp.common.lib.camera.Camera import Camera -from opencsp.common.lib.csp.Facet import Facet +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.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.file_tools as ft import opencsp.common.lib.tool.log_tools as lt def example_process_undefined_shape_facet(): - """Performs processing of previously collected Sofast data of single facet mirror: - - 1. Load saved single facet Sofast collection data - 2. Processes data with Sofast (without using facet file) - 3. Log best-fit parabolic focal lengths - 4. Plot slope magnitude - 5. Save slope data as HDF5 file + """Performs processing of previously collected SOFAST data of single facet mirror + with an unknown shape. + + 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 images files """ # General setup # ============= @@ -54,8 +90,43 @@ def example_process_undefined_shape_facet(): measurement = MeasurementSofastFringe.load_from_hdf(file_measurement) calibration = ImageCalibrationScaling.load_from_hdf(file_calibration) - # 2. Process data with Sofast - # =========================== + # 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]) + 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 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 + 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 + 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) surface = Surface2DParabolic(initial_focal_lengths_xy=(300.0, 300.0), robust_least_squares=True, downsample=10) @@ -65,35 +136,57 @@ def example_process_undefined_shape_facet(): # Instantiate sofast object sofast = Sofast(measurement, orientation, camera, display) - sofast.params.mask_keep_largest_area = True + sofast.params.mask.keep_largest_area = True # Process sofast.process_optic_undefined(surface) - # 3. Log best-fit parabolic focal lengths - # ======================================= - surf_coefs = sofast.data_calculation_facet[0].surf_coefs_facet - focal_lengths_xy = [1 / 4 / surf_coefs[2], 1 / 4 / surf_coefs[5]] - lt.info(f'Facet xy focal lengths (meters): ' f'{focal_lengths_xy[0]:.3f}, {focal_lengths_xy[1]:.3f}') - - # 4. Plot slope magnitude - # ======================= - - # Get optic representation - facet: Facet = sofast.get_optic() - - # Generate plots - figure_control = rcfg.RenderControlFigure(tile_array=(1, 1), tile_square=True) - axis_control_m = rca.meters() - - # Plot slope map - fig_record = fm.setup_figure(figure_control, axis_control_m, title='') - facet.plot_orthorectified_slope(res=0.002, clim=7, axis=fig_record.axis) - fig_record.save(dir_save, 'slope_magnitude', 'png') - - # 5. Save slope data as HDF5 file - # =============================== - sofast.save_to_hdf(f'{dir_save}/data_undefined.h5') + # 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_deviation_vis.clim = 1.5 + 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__': diff --git a/opencsp/app/sofast/lib/DefinitionEnsemble.py b/opencsp/app/sofast/lib/DefinitionEnsemble.py index eea2848b8..bc22c9f19 100644 --- a/opencsp/app/sofast/lib/DefinitionEnsemble.py +++ b/opencsp/app/sofast/lib/DefinitionEnsemble.py @@ -141,7 +141,7 @@ def save_to_hdf(self, file: str, prefix: str = '') -> None: hdf5_tools.save_hdf5_datasets(data, datasets, file) @classmethod - def load_from_hdf(cls, file: str, prefix: str) -> 'DefinitionEnsemble': + def load_from_hdf(cls, file: str, prefix: str = '') -> 'DefinitionEnsemble': """Loads DefinitionEnsemble object from given file. Data is stored in PREFIX + DefinitionEnsemble/... Parameters diff --git a/opencsp/app/sofast/lib/ParamsSofastFringe.py b/opencsp/app/sofast/lib/ParamsSofastFringe.py index fe06d7a73..30bf21ffb 100644 --- a/opencsp/app/sofast/lib/ParamsSofastFringe.py +++ b/opencsp/app/sofast/lib/ParamsSofastFringe.py @@ -51,8 +51,8 @@ def load_from_hdf(cls, file: str, prefix: str = ''): Default is empty string ''. """ # Load geometry parameters - params_mask = ParamsMaskCalculation.load_from_hdf(file, prefix + '/ParamsSofastFringe/') - params_geometry = ParamsOpticGeometry.load_from_hdf(file, prefix + '/ParamsSofastFringe/') + params_mask = ParamsMaskCalculation.load_from_hdf(file, prefix + 'ParamsSofastFringe/') + params_geometry = ParamsOpticGeometry.load_from_hdf(file, prefix + 'ParamsSofastFringe/') # Load sofast parameters data = {'geometry': params_geometry, 'mask': params_mask} diff --git a/opencsp/app/sofast/test/test_image_processing.py b/opencsp/app/sofast/test/test_image_processing.py index 1fe032780..7880c4e24 100644 --- a/opencsp/app/sofast/test/test_image_processing.py +++ b/opencsp/app/sofast/test/test_image_processing.py @@ -138,8 +138,8 @@ def test_refine_facet_corners(self): """Tests image_processing.refine_facet_corners()""" # Load edge data datasets = [ - 'DataSofastCalculation/image_processing/general/v_edges_image', - 'DataSofastInput/optic_definition/ensemble/r_facet_ensemble', + 'DataSofastCalculation/general/CalculationImageProcessingGeneral/v_edges_image', + 'DataSofastInput/optic_definition/DefinitionEnsemble/r_facet_ensemble', ] data = load_hdf5_datasets(datasets, self.data_file_multi) num_facets = data['r_facet_ensemble'].shape[0] @@ -151,9 +151,9 @@ def test_refine_facet_corners(self): for facet_idx in range(num_facets): # Get sofast parameters datasets = [ - 'DataSofastInput/sofast_params/facet_corns_refine_step_length', - 'DataSofastInput/sofast_params/facet_corns_refine_perpendicular_search_dist', - 'DataSofastInput/sofast_params/facet_corns_refine_frac_keep', + 'DataSofastInput/ParamsSofastFringe/ParamsOpticGeometry/facet_corns_refine_step_length', + 'DataSofastInput/ParamsSofastFringe/ParamsOpticGeometry/facet_corns_refine_perpendicular_search_dist', + 'DataSofastInput/ParamsSofastFringe/ParamsOpticGeometry/facet_corns_refine_frac_keep', ] data = load_hdf5_datasets(datasets, self.data_file_multi) args = [ @@ -164,9 +164,9 @@ def test_refine_facet_corners(self): # Load input and expected output data datasets = [ - f'DataSofastCalculation/image_processing/facet_{facet_idx:03d}/loop_facet_image_refine', - f'DataSofastCalculation/image_processing/facet_{facet_idx:03d}/v_facet_corners_image_exp', - f'DataSofastCalculation/image_processing/facet_{facet_idx:03d}/v_facet_centroid_image_exp', + f'DataSofastCalculation/facet/facet_{facet_idx:03d}/CalculationImageProcessingFacet/loop_facet_image_refine', + f'DataSofastCalculation/facet/facet_{facet_idx:03d}/CalculationImageProcessingFacet/v_facet_corners_image_exp', + f'DataSofastCalculation/facet/facet_{facet_idx:03d}/CalculationImageProcessingFacet/v_facet_centroid_image_exp', ] data = load_hdf5_datasets(datasets, self.data_file_multi) v_facet_corners_image_exp = Vxy(data['v_facet_corners_image_exp']) diff --git a/opencsp/app/sofast/test/test_integration_multi_facet.py b/opencsp/app/sofast/test/test_integration_multi_facet.py index fcdb71f35..ecee71cb6 100644 --- a/opencsp/app/sofast/test/test_integration_multi_facet.py +++ b/opencsp/app/sofast/test/test_integration_multi_facet.py @@ -1,10 +1,12 @@ """Integration test. Testing processing of a 'multi_facet' type optic. + +To update the test data, simply run this test and replace the input SOFAST +dataset HDF5 file with the output HDF5 file created when running this test. """ import os import unittest -from scipy.spatial.transform import Rotation import numpy as np from opencsp.app.sofast.lib.DisplayShape import DisplayShape @@ -16,8 +18,8 @@ from opencsp.app.sofast.lib.SpatialOrientation import SpatialOrientation from opencsp.common.lib.camera.Camera import Camera from opencsp.common.lib.deflectometry.Surface2DParabolic import Surface2DParabolic -from opencsp.common.lib.geometry.Vxyz import Vxyz from opencsp.common.lib.tool.hdf5_tools import load_hdf5_datasets +import opencsp.common.lib.tool.file_tools as ft from opencsp.common.lib.opencsp_path.opencsp_root_path import opencsp_code_dir @@ -32,6 +34,10 @@ def setUpClass(cls, base_dir: str | None = None): Sets base directory. If None, uses 'data' directory in directory contianing file, by default None """ + # Define save directory + cls.dir_save = os.path.join(os.path.dirname(__file__), 'data/output/sofast_ensemble') + ft.create_directories_if_necessary(cls.dir_save) + # Get test data location if base_dir is None: base_dir = os.path.join(opencsp_code_dir(), 'test/data/sofast_fringe') @@ -46,21 +52,13 @@ def setUpClass(cls, base_dir: str | None = None): orientation = SpatialOrientation.load_from_hdf(file_dataset) measurement = MeasurementSofastFringe.load_from_hdf(file_measurement) calibration = ImageCalibrationScaling.load_from_hdf(file_dataset) - - # Load sofast params - datasets = [ - 'DataSofastInput/sofast_params/mask_hist_thresh', - 'DataSofastInput/sofast_params/mask_filt_width', - 'DataSofastInput/sofast_params/mask_filt_thresh', - 'DataSofastInput/sofast_params/mask_thresh_active_pixels', - 'DataSofastInput/sofast_params/mask_keep_largest_area', - 'DataSofastInput/sofast_params/perimeter_refine_axial_search_dist', - 'DataSofastInput/sofast_params/perimeter_refine_perpendicular_search_dist', - 'DataSofastInput/sofast_params/facet_corns_refine_step_length', - 'DataSofastInput/sofast_params/facet_corns_refine_perpendicular_search_dist', - 'DataSofastInput/sofast_params/facet_corns_refine_frac_keep', - ] - params = load_hdf5_datasets(datasets, file_dataset) + data_ensemble = DefinitionEnsemble.load_from_hdf(file_dataset, 'DataSofastInput/optic_definition/') + data_facets = [] + data_surfaces = [] + for idx_facet in range(data_ensemble.num_facets): + prefix = f'DataSofastInput/optic_definition/facet_{idx_facet:03d}/' + data_surfaces.append(Surface2DParabolic.load_from_hdf(file_dataset, prefix)) + data_facets.append(DefinitionFacet.load_from_hdf(file_dataset, prefix)) # Calibrate measurement measurement.calibrate_fringe_images(calibration) @@ -68,72 +66,25 @@ def setUpClass(cls, base_dir: str | None = None): # Instantiate sofast object sofast = ProcessSofastFringe(measurement, orientation, camera, display) - # Update parameters - sofast.params.mask.hist_thresh = params['mask_hist_thresh'] - sofast.params.mask.filt_width = params['mask_filt_width'] - sofast.params.mask.filt_thresh = params['mask_filt_thresh'] - sofast.params.mask.thresh_active_pixels = params['mask_thresh_active_pixels'] - sofast.params.mask.keep_largest_area = params['mask_keep_largest_area'] - - sofast.params.geometry.perimeter_refine_axial_search_dist = params['perimeter_refine_axial_search_dist'] - sofast.params.geometry.perimeter_refine_perpendicular_search_dist = params[ - 'perimeter_refine_perpendicular_search_dist' - ] - sofast.params.geometry.facet_corns_refine_step_length = params['facet_corns_refine_step_length'] - sofast.params.geometry.facet_corns_refine_perpendicular_search_dist = params[ - 'facet_corns_refine_perpendicular_search_dist' - ] - sofast.params.geometry.facet_corns_refine_frac_keep = params['facet_corns_refine_frac_keep'] - - # Load array data - datasets = [ - 'DataSofastInput/optic_definition/ensemble/ensemble_perimeter', - 'DataSofastInput/optic_definition/ensemble/r_facet_ensemble', - 'DataSofastInput/optic_definition/ensemble/v_centroid_ensemble', - 'DataSofastInput/optic_definition/ensemble/v_facet_locations', - ] - ensemble_data = load_hdf5_datasets(datasets, file_dataset) - ensemble_data = DefinitionEnsemble( - Vxyz(ensemble_data['v_facet_locations']), - [Rotation.from_rotvec(r) for r in ensemble_data['r_facet_ensemble']], - ensemble_data['ensemble_perimeter'], - Vxyz(ensemble_data['v_centroid_ensemble']), - ) - - # Load facet data - facet_data = [] - for idx in range(len(ensemble_data.r_facet_ensemble)): - datasets = [ - f'DataSofastInput/optic_definition/facet_{idx:03d}/v_centroid_facet', - f'DataSofastInput/optic_definition/facet_{idx:03d}/v_facet_corners', - ] - data = load_hdf5_datasets(datasets, file_dataset) - facet_data.append(DefinitionFacet(Vxyz(data['v_facet_corners']), Vxyz(data['v_centroid_facet']))) - - # Load surface data - surfaces = [] - for idx in range(len(facet_data)): - datasets = [ - f'DataSofastInput/surface_params/facet_{idx:03d}/downsample', - f'DataSofastInput/surface_params/facet_{idx:03d}/initial_focal_lengths_xy', - f'DataSofastInput/surface_params/facet_{idx:03d}/robust_least_squares', - ] - data = load_hdf5_datasets(datasets, file_dataset) - data['robust_least_squares'] = bool(data['robust_least_squares']) - surfaces.append(Surface2DParabolic(**data)) + # Update sofast processing parameters + sofast.params = sofast.params.load_from_hdf(file_dataset, 'DataSofastInput/') # Run SOFAST - sofast.process_optic_multifacet(facet_data, ensemble_data, surfaces) + sofast.process_optic_multifacet(data_facets, data_ensemble, data_surfaces) # Store data - cls.data_test = {'slopes_facet_xy': [], 'surf_coefs_facet': []} + cls.data_test = {'slopes_facet_xy': [], 'surf_coefs_facet': [], 'facet_pointing': []} cls.num_facets = sofast.num_facets cls.file_dataset = file_dataset + cls.sofast = sofast for idx in range(sofast.num_facets): cls.data_test['slopes_facet_xy'].append(sofast.data_calculation_facet[idx].slopes_facet_xy) cls.data_test['surf_coefs_facet'].append(sofast.data_calculation_facet[idx].surf_coefs_facet) + cls.data_test['facet_pointing'].append( + sofast.data_calculation_ensemble[idx].v_facet_pointing_ensemble.data.squeeze() + ) def test_slope(self): for idx in range(self.num_facets): @@ -142,7 +93,7 @@ def test_slope(self): data_calc = self.data_test['slopes_facet_xy'][idx] # Get expected data - datasets = [f'DataSofastCalculation/facet/facet_{idx:03d}/slopes_facet_xy'] + datasets = [f'DataSofastCalculation/facet/facet_{idx:03d}/SlopeSolverData/slopes_facet_xy'] data = load_hdf5_datasets(datasets, self.file_dataset) # Test @@ -155,12 +106,30 @@ def test_surf_coefs(self): data_calc = self.data_test['surf_coefs_facet'][idx] # Get expected data - datasets = [f'DataSofastCalculation/facet/facet_{idx:03d}/surf_coefs_facet'] + datasets = [f'DataSofastCalculation/facet/facet_{idx:03d}/SlopeSolverData/surf_coefs_facet'] data = load_hdf5_datasets(datasets, self.file_dataset) # Test np.testing.assert_allclose(data['surf_coefs_facet'], data_calc, atol=1e-8, rtol=0) + def test_facet_pointing(self): + for idx in range(self.num_facets): + with self.subTest(i=idx): + # Get calculated data + data_calc = self.data_test['facet_pointing'][idx] + + # Get expected data + datasets = [ + f'DataSofastCalculation/facet/facet_{idx:03d}/CalculationEnsemble/v_facet_pointing_ensemble' + ] + data = load_hdf5_datasets(datasets, self.file_dataset) + + # Test + np.testing.assert_allclose(data['v_facet_pointing_ensemble'], data_calc, atol=1e-8, rtol=0) + + def test_save_as_hdf5(self): + self.sofast.save_to_hdf(os.path.join(self.dir_save, 'sofast_processed_data_ensemble.h5')) + if __name__ == '__main__': unittest.main() diff --git a/opencsp/common/lib/csp/FacetEnsemble.py b/opencsp/common/lib/csp/FacetEnsemble.py index 797e857c7..bfc8632da 100644 --- a/opencsp/common/lib/csp/FacetEnsemble.py +++ b/opencsp/common/lib/csp/FacetEnsemble.py @@ -192,7 +192,7 @@ def draw( def set_facet_transform_list(self, transformations: list[TransformXYZ]): """ - Combines the `set_facet_positions` and `set_facet_canting` functions into a single action. + Combines the `set_facet_positions` and `set_facet_cantings` functions into a single action. """ for transformation, facet in zip(transformations, self.facets): facet._self_to_parent_transform = transformation @@ -231,7 +231,7 @@ def set_facet_positions(self, positions: Pxyz): pos: Pxyz facet._self_to_parent_transform = TransformXYZ.from_V(pos) - def set_facet_canting(self, canting_rotations: list[Rotation]): + def set_facet_cantings(self, canting_rotations: list[Rotation]): """ Set the canting rotations of the facets relative to the ensemble. This function updates the canting rotations of the facets in the diff --git a/opencsp/common/lib/csp/HeliostatAbstract.py b/opencsp/common/lib/csp/HeliostatAbstract.py index bbc5bb1bb..92a4163f3 100644 --- a/opencsp/common/lib/csp/HeliostatAbstract.py +++ b/opencsp/common/lib/csp/HeliostatAbstract.py @@ -221,8 +221,8 @@ def set_tracking_configuration(self, aimpoint: Pxyz, location_lon_lat: Iterable, def set_facet_positions(self, positions: Pxyz): self.facet_ensemble.set_facet_positions(positions) - def set_facet_canting(self, canting_rotations: list[Rotation]): - self.facet_ensemble.set_facet_canting(canting_rotations) + def set_facet_cantings(self, canting_rotations: list[Rotation]): + self.facet_ensemble.set_facet_cantings(canting_rotations) # TODO TJL:make this work and make it faster def set_canting_from_equation(self, func: FunctionXYContinuous) -> None: @@ -272,7 +272,7 @@ def set_canting_from_equation(self, func: FunctionXYContinuous) -> None: facet_canting_rotations.append(canting) - self.facet_ensemble.set_facet_canting(facet_canting_rotations) + self.facet_ensemble.set_facet_cantings(facet_canting_rotations) # RENDERING diff --git a/opencsp/common/lib/test/test_MirrorOutput.py b/opencsp/common/lib/test/test_MirrorOutput.py index 1548cc4b5..d1afdc540 100644 --- a/opencsp/common/lib/test/test_MirrorOutput.py +++ b/opencsp/common/lib/test/test_MirrorOutput.py @@ -129,7 +129,7 @@ def setUp(self): tilt_left * tilt_down, tilt_right * tilt_down, ] - fe2x2.set_facet_canting(fe_2x2_canting_rotations) + fe2x2.set_facet_cantings(fe_2x2_canting_rotations) self.h2x2 = HeliostatAzEl(fe2x2, name='Simple 2x2 Heliostat') self.h2x2_title = 'Heliostat with Parametrically Defined Facets' diff --git a/opencsp/common/lib/test/test_RayTraceOutput.py b/opencsp/common/lib/test/test_RayTraceOutput.py index 241de57cf..70bf749d0 100644 --- a/opencsp/common/lib/test/test_RayTraceOutput.py +++ b/opencsp/common/lib/test/test_RayTraceOutput.py @@ -130,7 +130,7 @@ def setUp(self): self.fe2x2 = FacetEnsemble(self.h2x2_facets) fe2x2_positions = Pxyz([[-1.1, 1.1, -1.1, 1.1], [1.6, 1.6, -1.6, -1.6], [0, 0, 0, 0]]) self.fe2x2.set_facet_positions(fe2x2_positions) - self.fe2x2.set_facet_canting(self.h2x2_canting) + self.fe2x2.set_facet_cantings(self.h2x2_canting) self.h2x2 = HeliostatAzEl(self.fe2x2, 'Simple 2x2 Heliostat') self.h2x2.pivot = 0 self.h2x2_title = 'Heliostat with Parametrically Defined Facets' diff --git a/opencsp/test/data/sofast_fringe/data_expected_facet_ensemble/data.h5 b/opencsp/test/data/sofast_fringe/data_expected_facet_ensemble/data.h5 index 3d120ceec..195f7af9b 100644 Binary files a/opencsp/test/data/sofast_fringe/data_expected_facet_ensemble/data.h5 and b/opencsp/test/data/sofast_fringe/data_expected_facet_ensemble/data.h5 differ