From 0eb39557268966779b428adf669590d43b135e34 Mon Sep 17 00:00:00 2001 From: crnh <30109443+crnh@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:53:14 +0100 Subject: [PATCH 01/18] Add analysis for cross section viewer --- zospy/analyses/new/__init__.py | 3 +- zospy/analyses/new/base.py | 4 +- zospy/analyses/new/systemviewers/__init__.py | 5 + zospy/analyses/new/systemviewers/base.py | 186 ++++++++++++++++++ .../new/systemviewers/cross_section.py | 91 +++++++++ 5 files changed, 286 insertions(+), 3 deletions(-) create mode 100644 zospy/analyses/new/systemviewers/__init__.py create mode 100644 zospy/analyses/new/systemviewers/base.py create mode 100644 zospy/analyses/new/systemviewers/cross_section.py diff --git a/zospy/analyses/new/__init__.py b/zospy/analyses/new/__init__.py index 7d84472..42ad89d 100644 --- a/zospy/analyses/new/__init__.py +++ b/zospy/analyses/new/__init__.py @@ -20,7 +20,7 @@ ... ) """ -from zospy.analyses.new import mtf, polarization, raysandspots, reports, surface, wavefront +from zospy.analyses.new import mtf, polarization, raysandspots, reports, surface, systemviewers, wavefront __all__ = ( "mtf", @@ -28,5 +28,6 @@ "raysandspots", "reports", "surface", + "systemviewers", "wavefront", ) diff --git a/zospy/analyses/new/base.py b/zospy/analyses/new/base.py index fd47529..1aae603 100644 --- a/zospy/analyses/new/base.py +++ b/zospy/analyses/new/base.py @@ -592,12 +592,12 @@ def get_text_output(self) -> str: with open(self._text_output_file, encoding=self.oss._ZOS.get_txtfile_encoding()) as f: # noqa: SLF001 return f.read() - def _create_analysis(self): + def _create_analysis(self, *, settings_first=True): if self.analysis is not None and self.analysis.TypeName == self.TYPE: return analysis_type = constants.process_constant(constants.Analysis.AnalysisIDM, self.TYPE) - self._analysis = new_analysis(self.oss, analysis_type) + self._analysis = new_analysis(self.oss, analysis_type, settings_first=settings_first) @abstractmethod def run_analysis(self, *args, **kwargs) -> AnalysisData: diff --git a/zospy/analyses/new/systemviewers/__init__.py b/zospy/analyses/new/systemviewers/__init__.py new file mode 100644 index 0000000..00e626d --- /dev/null +++ b/zospy/analyses/new/systemviewers/__init__.py @@ -0,0 +1,5 @@ +"""OpticStudio system viewers.""" + +from zospy.analyses.new.systemviewers.cross_section import CrossSection, CrossSectionSettings + +__all__ = ("CrossSection", "CrossSectionSettings") diff --git a/zospy/analyses/new/systemviewers/base.py b/zospy/analyses/new/systemviewers/base.py new file mode 100644 index 0000000..a64d9f7 --- /dev/null +++ b/zospy/analyses/new/systemviewers/base.py @@ -0,0 +1,186 @@ +from __future__ import annotations + +import weakref +from abc import ABC, abstractmethod +from dataclasses import fields +from os import PathLike +from pathlib import Path +from typing import TYPE_CHECKING, Generic, Literal +from warnings import warn + +import numpy as np +from pydantic.fields import FieldInfo +from System import Array + +from zospy.analyses.new.base import AnalysisData, AnalysisResult, AnalysisSettings, BaseAnalysisWrapper, OnComplete +from zospy.utils.pyutils import abspath + +if TYPE_CHECKING: + from zospy.api import _ZOSAPI + from zospy.zpcore import OpticStudioSystem + + +class SystemViewerWrapper(BaseAnalysisWrapper[np.ndarray | None, AnalysisSettings], ABC, Generic[AnalysisSettings]): + """Base class for SystemViewer analyses.""" + + ALLOWED_IMAGE_EXTENSIONS: tuple[str, ...] = ("bmp", "jpeg", "png") + + def __init__(self, settings: AnalysisSettings, settings_arguments: dict[str, any]): + super().__init__(settings, settings_arguments) + + self._image_output_file = None + + @property + def image_output_file(self) -> str | Path | None: + """Path to the image output file.""" + return self._image_output_file + + def _warn_ignored_settings(self) -> None: + """Check if unsupported parameters are specified and warn the user. + + For OpticStudio versions below 24R1, compare the values of a dictionary with the default values of a function, + and warn if any are different. + """ + changed_parameters = [] + + for field in fields(type(self.settings)): + default = field.default.default if isinstance(field.default, FieldInfo) else field.default + + if getattr(self.settings, field.name) != default: + changed_parameters.append(field.name) + + if len(changed_parameters) > 0: + warn( + f"Some parameters were specified but ignored, because viewer exports are only supported from OpticStudio" + f"24R1: {', '.join(changed_parameters)}" + ) + + def _validate_path(self, path: PathLike | str) -> str: + str_path = abspath(path, check_directory_only=True) + + if str_path.split(".")[-1] in self.ALLOWED_IMAGE_EXTENSIONS: + return str_path + + raise ValueError(f"Image file must have one of the following extensions: {self.ALLOWED_IMAGE_EXTENSIONS}") + + def _validate_wavelength(self, wavelength: int | str) -> int: + if isinstance(wavelength, str): + if wavelength == "All": + return -1 + + raise ValueError("wavelength must be an integer or 'All'.") + + if wavelength < -1 or wavelength > self.oss.SystemData.Wavelengths.NumberOfWavelengths: + raise ValueError("wavelength must be -1 or between 1 and the number of wavelengths.") + + return wavelength + + def _validate_field(self, field: int | str) -> int: + if isinstance(field, str): + if field == "All": + return -1 + + raise ValueError("field must be an integer or 'All'.") + + if field < 1 or field > self.oss.SystemData.Fields.NumberOfFields: + raise ValueError("field must be -1 or between 1 and the number of fields.") + + return field + + def _validate_end_surface(self, start_surface, end_surface: int): + if end_surface != -1 and end_surface <= start_surface or end_surface > self.oss.LDE.NumberOfSurfaces - 1: + raise ValueError( + "end_surface must be -1 or greater than start_surface and less than the number of surfaces." + ) + + if end_surface == -1: + return self.oss.LDE.NumberOfSurfaces - 1 + + return end_surface + + def _close_current_tool(self) -> None: + """Close the current tool in OpticStudio.""" + if self.oss.Tools.CurrentTool is not None: + self.oss.Tools.CurrentTool.Close() + + @staticmethod + def _get_image_data(image_data: _ZOSAPI.Tools.Layouts.IImageExportData | None) -> np.ndarray | None: + if image_data is None: + return image_data + + image_size = image_data.Width * image_data.Height + + # In-place updating arrays works only with dotnet arrays + r_values = Array[int](image_size) # [0] * image_size + g_values = Array[int](image_size) # [0] * image_size + b_values = Array[int](image_size) # [0] * image_size + + image_data.FillValues(image_data.Width * image_data.Height, r_values, g_values, b_values) + + return np.stack((r_values, g_values, b_values), axis=-1).reshape(image_data.Height, image_data.Width, 3) + + @abstractmethod + def configure_layout_tool( + self, + ) -> _ZOSAPI.Tools.Layouts.ICrossSectionExport | _ZOSAPI.Tools.Layouts.I3DViewerExport: + """Configure the layout tool for the analysis.""" + + def run_analysis(self) -> np.ndarray | None: + """Run the layout tool.""" + layout_tool = self.configure_layout_tool() + + if self.image_output_file is not None: + layout_tool.SaveImageAsFile = True + layout_tool.OutputFileName = self.image_output_file + else: + layout_tool.SaveImageAsFile = False + + layout_tool.RunAndWaitForCompletion() + + if not layout_tool.Succeeded: + raise RuntimeError("The system viewer export tool failed to run.") + + image_data = self._get_image_data(layout_tool.ImageExportData) if self.image_output_file is None else None + + layout_tool.Close() + + return image_data + + def run( + self, + oss: OpticStudioSystem, + image_output_file: str | Path | None = None, + oncomplete: OnComplete | Literal["Close", "Release", "Sustain"] = "Close", + **kwargs, + ) -> AnalysisData: + """Run the analysis. + + Parameters + ---------- + **kwargs + """ + if image_output_file is not None: + self._image_output_file = self._validate_path(image_output_file) + + self._oss = weakref.proxy(oss) + self._create_analysis(settings_first=False) + + image_data = None + + if self.oss._ZOS.version >= (24, 1, 0): # noqa: SLF001 + self._close_current_tool() + image_data = self.run_analysis() + else: + self._warn_ignored_settings() + + result = AnalysisResult( + image_data, + settings=self.settings, + metadata=self.analysis.metadata, + header=self.analysis.header_data, + messages=self.analysis.messages, + ) + + self._complete(OnComplete(oncomplete)) + + return result diff --git a/zospy/analyses/new/systemviewers/cross_section.py b/zospy/analyses/new/systemviewers/cross_section.py new file mode 100644 index 0000000..0a3cba3 --- /dev/null +++ b/zospy/analyses/new/systemviewers/cross_section.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +from typing import Annotated, Literal + +import numpy as np +from pydantic.dataclasses import Field + +from zospy.analyses.new.decorators import analysis_settings +from zospy.analyses.new.parsers.types import FieldNumber, WavelengthNumber +from zospy.analyses.new.systemviewers.base import SystemViewerWrapper +from zospy.api import constants + +__all__ = ("CrossSection", "CrossSectionSettings") + + +@analysis_settings +class CrossSectionSettings: + """Settings for the cross section viewer.""" + + start_surface: int = Field(default=1, ge=1, description="Starting surface number") + end_surface: Literal[-1] | Annotated[int, Field(ge=1)] = Field(default=-1, description="Ending surface number") + number_of_rays: int = Field(default=3, ge=1, description="Number of rays") + y_stretch: float = Field(default=1.0, ge=0, description="Y stretch factor") + fletch_rays: bool = Field(default=False, description="Fletch rays") + wavelength: WavelengthNumber = Field(default="All", description="Wavelength number") + field: FieldNumber = Field(default="All", description="Field number") + color_rays_by: str = Field(default="Fields", description="Color rays by") + upper_pupil: float = Field(default=1.0, gt=-1, le=1, description="Upper pupil limit") + lower_pupil: float = Field(default=-1.0, ge=-1, lt=1, description="Lower pupil limit") + delete_vignetted: bool = Field(default=False, description="Delete vignetted rays") + marginal_and_chief_only: bool = Field(default=False, description="Draw marginal and chief rays only") + image_size: tuple[int, int] = Field(default=(800, 600), description="Image size") + rays_line_thickness: str = Field(default="Standard", description="Rays line thickness") + surface_line_thickness: str = Field(default="Standard", description="Surface line thickness") + + +class CrossSection(SystemViewerWrapper[CrossSectionSettings]): + """Cross section viewer.""" + + TYPE = "Draw2D" + MODE = "Sequential" + + def __init__( + self, + *, + start_surface: int = 1, + end_surface: int = -1, + number_of_rays: int = 3, + y_stretch: float = 1.0, + fletch_rays: bool = False, + wavelength: int | Literal["All"] = "All", + field: int | Literal["All"] = "All", + color_rays_by: constants.Tools.Layouts.ColorRaysByCrossSectionOptions | str = "Fields", + upper_pupil: float = 1.0, + lower_pupil: float = -1.0, + delete_vignetted: bool = False, + marginal_and_chief_only: bool = False, + image_size: tuple[int, int] = (800, 600), + rays_line_thickness: constants.Tools.Layouts.LineThicknessOptions | str = "Standard", + surface_line_thickness: constants.Tools.Layouts.LineThicknessOptions | str = "Standard", + settings: CrossSectionSettings | None = None, + ): + """Initialize the cross section viewer.""" + super().__init__(settings or CrossSectionSettings(), locals()) + + def configure_layout_tool(self) -> np.ndarray | None: + """Run the cross section viewer.""" + layout_tool = self.oss.Tools.Layouts.OpenCrossSectionExport() + layout_tool.StartSurface = self.settings.start_surface + layout_tool.EndSurface = self._validate_end_surface(self.settings.start_surface, self.settings.end_surface) + layout_tool.NumberOfRays = self.settings.number_of_rays + layout_tool.YStretch = self.settings.y_stretch + layout_tool.FletchRays = self.settings.fletch_rays + layout_tool.Wavelength = self._validate_wavelength(self.settings.wavelength) + layout_tool.Field = self._validate_field(self.settings.field) + layout_tool.ColorRaysBy = constants.process_constant( + constants.Tools.Layouts.ColorRaysByCrossSectionOptions, self.settings.color_rays_by + ) + layout_tool.UpperPupil = self.settings.upper_pupil + layout_tool.LowerPupil = self.settings.lower_pupil + layout_tool.DeleteVignetted = self.settings.delete_vignetted + layout_tool.MarginalAndChiefOnly = self.settings.marginal_and_chief_only + layout_tool.OutputPixelHeight, layout_tool.OutputPixelWidth = self.settings.image_size + layout_tool.RaysLineThickness = constants.process_constant( + constants.Tools.Layouts.LineThicknessOptions, self.settings.rays_line_thickness + ) + layout_tool.SurfaceLineThickness = constants.process_constant( + constants.Tools.Layouts.LineThicknessOptions, self.settings.surface_line_thickness + ) + + return layout_tool From f39fd47bdf842469f51bc3c76a7cf8c6cf287a8c Mon Sep 17 00:00:00 2001 From: crnh <30109443+crnh@users.noreply.github.com> Date: Wed, 11 Dec 2024 18:58:35 +0100 Subject: [PATCH 02/18] Add support for checking the system mode --- zospy/analyses/new/base.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/zospy/analyses/new/base.py b/zospy/analyses/new/base.py index 1aae603..9e925f8 100644 --- a/zospy/analyses/new/base.py +++ b/zospy/analyses/new/base.py @@ -496,6 +496,7 @@ class BaseAnalysisWrapper(ABC, Generic[AnalysisData, AnalysisSettings]): """ TYPE: str = None + MODE: Literal["Sequential", "Nonsequential"] | None = None # Flags to indicate if the analysis needs a configuration file or text output file _needs_config_file: bool = False @@ -603,6 +604,13 @@ def _create_analysis(self, *, settings_first=True): def run_analysis(self, *args, **kwargs) -> AnalysisData: """Run the analysis and return the results.""" + def _check_mode(self): + if self.MODE is None: + return + + if self.oss.Mode != self.MODE: + raise ValueError(f"The analysis requires {self.MODE} mode, got {self.oss.Mode}.") + @staticmethod def _create_tempfile(path: Path | None, suffix: str) -> (Path, bool): if path is None: @@ -663,6 +671,7 @@ def run( The analysis results. """ self._oss = weakref.proxy(oss) + self._check_mode() self._create_analysis() if self._needs_config_file: From 2b5181358962dd62c4434a66fe51d85af3e45422 Mon Sep 17 00:00:00 2001 From: crnh <30109443+crnh@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:32:55 +0100 Subject: [PATCH 03/18] Pydantic validation of image size and ZOSAPI constants --- zospy/analyses/new/parsers/types.py | 46 +++++++++++++-- zospy/analyses/new/systemviewers/base.py | 11 +++- .../new/systemviewers/cross_section.py | 59 ++++++++++++++++--- 3 files changed, 102 insertions(+), 14 deletions(-) diff --git a/zospy/analyses/new/parsers/types.py b/zospy/analyses/new/parsers/types.py index bdac527..2b2fc90 100644 --- a/zospy/analyses/new/parsers/types.py +++ b/zospy/analyses/new/parsers/types.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Annotated, Generic, Literal, TypeVar, Union +from operator import attrgetter +from typing import TYPE_CHECKING, Annotated, Any, Generic, Literal, TypeVar, Union from numpy import array, ndarray from pandas import DataFrame @@ -10,11 +11,14 @@ from pydantic.dataclasses import dataclass from pydantic_core import CoreSchema, PydanticCustomError, core_schema -__all__ = ("UnitField", "ValidatedDataFrame", "ValidatedNDArray", "WavelengthNumber", "FieldNumber") +from zospy.api import constants if TYPE_CHECKING: from pydantic import GetCoreSchemaHandler +__all__ = ("UnitField", "ValidatedDataFrame", "ValidatedNDArray", "WavelengthNumber", "FieldNumber", "ZOSAPIConstant") + + Value = TypeVar("Value") @@ -49,7 +53,7 @@ def _serialize_dataframe(value: DataFrame) -> dict: return value.to_dict(orient="tight") @classmethod - def __get_pydantic_core_schema__(cls, source_type: any, handler: GetCoreSchemaHandler) -> CoreSchema: + def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema: schema = core_schema.json_or_python_schema( json_schema=core_schema.dict_schema(), python_schema=core_schema.union_schema( @@ -93,7 +97,7 @@ def _validate_ndarray(value: list | ndarray) -> ndarray: return value @classmethod - def __get_pydantic_core_schema__(cls, source_type: any, handler: GetCoreSchemaHandler) -> CoreSchema: + def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema: schema = core_schema.json_or_python_schema( json_schema=core_schema.list_schema(), python_schema=core_schema.union_schema( @@ -114,5 +118,39 @@ def __get_pydantic_core_schema__(cls, source_type: any, handler: GetCoreSchemaHa ValidatedNDArray = Annotated[ndarray, ValidatedNDArrayAnnotation] +ZospyConstantType = TypeVar("ZospyConstantType") + + +class ZOSAPIConstantAnnotation: + """Pydantic validation and serialization for ZOSAPI constants.""" + + def __init__(self, enum: str): + self.enum = enum + + def _validate_constant(self, value: ZospyConstantType | str) -> ZospyConstantType: + try: + constant = attrgetter(self.enum)(constants) + except AttributeError as e: + raise AttributeError(f"Constant {self.enum} not found in zospy.constants") from e + + return constants.process_constant(constant, value) + + def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaHandler) -> any: + """Validate ZOSAPI constants.""" + schema = core_schema.json_or_python_schema( + json_schema=core_schema.any_schema(), + python_schema=core_schema.any_schema(), + ) + + serializer = core_schema.plain_serializer_function_ser_schema(str, when_used="json-unless-none") + + return core_schema.no_info_after_validator_function(self._validate_constant, schema, serialization=serializer) + + +def ZOSAPIConstant(enum: str) -> type[str]: # noqa: N802 + """Pydantic validation and serialization for ZOSAPI constants.""" + return Annotated[str, ZOSAPIConstantAnnotation(enum)] + + WavelengthNumber = Union[Literal["All"], Annotated[int, Field(gt=0)]] FieldNumber = Union[Literal["All"], Annotated[int, Field(gt=0)]] diff --git a/zospy/analyses/new/systemviewers/base.py b/zospy/analyses/new/systemviewers/base.py index a64d9f7..0a63079 100644 --- a/zospy/analyses/new/systemviewers/base.py +++ b/zospy/analyses/new/systemviewers/base.py @@ -5,11 +5,11 @@ from dataclasses import fields from os import PathLike from pathlib import Path -from typing import TYPE_CHECKING, Generic, Literal +from typing import TYPE_CHECKING, Annotated, Generic, Literal from warnings import warn import numpy as np -from pydantic.fields import FieldInfo +from pydantic.fields import Field, FieldInfo from System import Array from zospy.analyses.new.base import AnalysisData, AnalysisResult, AnalysisSettings, BaseAnalysisWrapper, OnComplete @@ -20,6 +20,9 @@ from zospy.zpcore import OpticStudioSystem +__all__ = ("SystemViewerWrapper", "ImageSize") + + class SystemViewerWrapper(BaseAnalysisWrapper[np.ndarray | None, AnalysisSettings], ABC, Generic[AnalysisSettings]): """Base class for SystemViewer analyses.""" @@ -184,3 +187,7 @@ def run( self._complete(OnComplete(oncomplete)) return result + + +_UInt = Annotated[int, Field(gt=0)] +ImageSize = tuple[_UInt, _UInt] diff --git a/zospy/analyses/new/systemviewers/cross_section.py b/zospy/analyses/new/systemviewers/cross_section.py index 0a3cba3..71b6104 100644 --- a/zospy/analyses/new/systemviewers/cross_section.py +++ b/zospy/analyses/new/systemviewers/cross_section.py @@ -1,3 +1,5 @@ +"""Cross section (2D) viewer.""" + from __future__ import annotations from typing import Annotated, Literal @@ -6,8 +8,8 @@ from pydantic.dataclasses import Field from zospy.analyses.new.decorators import analysis_settings -from zospy.analyses.new.parsers.types import FieldNumber, WavelengthNumber -from zospy.analyses.new.systemviewers.base import SystemViewerWrapper +from zospy.analyses.new.parsers.types import FieldNumber, WavelengthNumber, ZOSAPIConstant +from zospy.analyses.new.systemviewers.base import ImageSize, SystemViewerWrapper from zospy.api import constants __all__ = ("CrossSection", "CrossSectionSettings") @@ -15,7 +17,42 @@ @analysis_settings class CrossSectionSettings: - """Settings for the cross section viewer.""" + """Settings for the cross section viewer. + + Attributes + ---------- + start_surface : int, optional + The starting surface index for the cross-section analysis. Defaults to 1. + end_surface : int, optional + The ending surface index for the cross-section analysis. A value of -1 indicates the last surface. + Defaults to -1. + number_of_rays : int, optional + The number of rays to be used in the analysis. Defaults to 3. + y_stretch : float, optional + The stretch factor in the Y-axis for the analysis visualization. Defaults to 1.0. + fletch_rays : bool, optional + Flag indicating whether to fletch rays. Defaults to False. + wavelength : int | str, optional + The wavelength index to be used. Can be an integer or "all" for all wavelengths. Defaults to "all". + field : int | str, optional + The field index to be used. Can be an integer or "all" for all fields. Defaults to "all". + color_rays_by : constants.Tools.Layouts.ColorRaysByCrossSectionOptions | str, optional + The criterion for coloring rays in the analysis. Defaults to "Fields". + upper_pupil : float, optional + The upper pupil limit for the analysis. Defaults to 1. + lower_pupil : float, optional + The lower pupil limit for the analysis. Defaults to -1. + delete_vignetted : bool, optional + Flag indicating whether to delete vignetted rays. Defaults to False. + marginal_and_chief_only : bool, optional + Flag indicating whether to include only marginal and chief rays in the analysis. Defaults to False. + image_size : tuple[int, int], optional + The size of the output image in pixels (width, height). Defaults to (800, 600). + rays_line_thickness : constants.Tools.Layouts.LineThicknessOptions | str, optional + The thickness of the lines in the output visualization. Defaults to "Standard". + surface_line_thickness : constants.Tools.Layouts.LineThicknessOptions | str, optional + The thickness of the surface lines in the output visualization. Defaults to "Standard". + """ start_surface: int = Field(default=1, ge=1, description="Starting surface number") end_surface: Literal[-1] | Annotated[int, Field(ge=1)] = Field(default=-1, description="Ending surface number") @@ -24,14 +61,20 @@ class CrossSectionSettings: fletch_rays: bool = Field(default=False, description="Fletch rays") wavelength: WavelengthNumber = Field(default="All", description="Wavelength number") field: FieldNumber = Field(default="All", description="Field number") - color_rays_by: str = Field(default="Fields", description="Color rays by") + color_rays_by: ZOSAPIConstant("Tools.Layouts.ColorRaysByCrossSectionOptions") = Field( + default="Fields", description="Color rays by" + ) upper_pupil: float = Field(default=1.0, gt=-1, le=1, description="Upper pupil limit") lower_pupil: float = Field(default=-1.0, ge=-1, lt=1, description="Lower pupil limit") delete_vignetted: bool = Field(default=False, description="Delete vignetted rays") marginal_and_chief_only: bool = Field(default=False, description="Draw marginal and chief rays only") - image_size: tuple[int, int] = Field(default=(800, 600), description="Image size") - rays_line_thickness: str = Field(default="Standard", description="Rays line thickness") - surface_line_thickness: str = Field(default="Standard", description="Surface line thickness") + image_size: ImageSize = Field(default=(800, 600), description="Image size") + rays_line_thickness: ZOSAPIConstant("Tools.Layouts.LineThicknessOptions") = Field( + default="Standard", description="Rays line thickness" + ) + surface_line_thickness: ZOSAPIConstant("Tools.Layouts.LineThicknessOptions") = Field( + default="Standard", description="Surface line thickness" + ) class CrossSection(SystemViewerWrapper[CrossSectionSettings]): @@ -64,7 +107,7 @@ def __init__( super().__init__(settings or CrossSectionSettings(), locals()) def configure_layout_tool(self) -> np.ndarray | None: - """Run the cross section viewer.""" + """Configure the cross section viewer.""" layout_tool = self.oss.Tools.Layouts.OpenCrossSectionExport() layout_tool.StartSurface = self.settings.start_surface layout_tool.EndSurface = self._validate_end_surface(self.settings.start_surface, self.settings.end_surface) From a284837aa08f73536755a9e8ff9b2b0f3493c71d Mon Sep 17 00:00:00 2001 From: crnh <30109443+crnh@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:33:56 +0100 Subject: [PATCH 04/18] Add support for 3D viewer --- zospy/analyses/new/systemviewers/__init__.py | 3 +- zospy/analyses/new/systemviewers/viewer_3d.py | 221 ++++++++++++++++++ 2 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 zospy/analyses/new/systemviewers/viewer_3d.py diff --git a/zospy/analyses/new/systemviewers/__init__.py b/zospy/analyses/new/systemviewers/__init__.py index 00e626d..a5fb3a8 100644 --- a/zospy/analyses/new/systemviewers/__init__.py +++ b/zospy/analyses/new/systemviewers/__init__.py @@ -1,5 +1,6 @@ """OpticStudio system viewers.""" from zospy.analyses.new.systemviewers.cross_section import CrossSection, CrossSectionSettings +from zospy.analyses.new.systemviewers.viewer_3d import Viewer3D, Viewer3DSettings -__all__ = ("CrossSection", "CrossSectionSettings") +__all__ = ("CrossSection", "CrossSectionSettings", "Viewer3D", "Viewer3DSettings") diff --git a/zospy/analyses/new/systemviewers/viewer_3d.py b/zospy/analyses/new/systemviewers/viewer_3d.py new file mode 100644 index 0000000..7d91270 --- /dev/null +++ b/zospy/analyses/new/systemviewers/viewer_3d.py @@ -0,0 +1,221 @@ +"""3D system viewer.""" + +from __future__ import annotations + +from typing import Annotated, Literal + +from pydantic import Field + +from zospy.analyses.new.decorators import analysis_settings +from zospy.analyses.new.parsers.types import FieldNumber, WavelengthNumber, ZOSAPIConstant # noqa: TCH001 +from zospy.analyses.new.systemviewers.base import ImageSize, SystemViewerWrapper +from zospy.api import _ZOSAPI, constants + +__all__ = ("Viewer3D", "Viewer3DSettings") + + +@analysis_settings +class Viewer3DSettings: + """Settings for the 3D system viewer. + + Attributes + ---------- + start_surface : int, optional + The starting surface index for the 3D viewer. Defaults to 1. + end_surface : int, optional + The ending surface index for the 3D viewer. A value of -1 indicates the last surface. Defaults to -1. + number_of_rays : int, optional + The number of rays to be used in the analysis. Defaults to 3. + wavelength : int | str, optional + The wavelength index to be used. Can be an integer or "All" for all wavelengths. Defaults to "All". + field : int | str, optional + The field index to be used. Can be an integer or "All" for all fields. Defaults to "All". + ray_pattern : constants.Tools.General.RayPatternType | str, optional + The ray pattern to be used in the analysis. Defaults to "XYFan". + color_rays_by : constants.Tools.Layouts.ColorRaysByOptions | str, optional + The criterion for coloring rays in the analysis. Defaults to "Fields". + delete_vignetted : bool, optional + Flag indicating whether to delete vignetted rays. Defaults to False. + hide_lens_faces : bool, optional + Flag indicating whether to hide lens faces. Defaults to False. + hide_lens_edges : bool, optional + Flag indicating whether to hide lens edges. Defaults to False. + hide_x_bars : bool, optional + Flag indicating whether to hide the x-component of lens faces. Defaults to False. + draw_paraxial_pupils : bool, optional + Flag indicating whether to draw paraxial entrance and exit pupils. Defaults to False. + fletch_rays : bool, optional + Flag indicating whether to draw small arrows indicating the direction of the rays. Defaults to False. + split_nsc_rays : bool, optional + Flag indicating whether to split rays from non-sequential sources at ray-surface intercepts. Defaults to False. + scatter_nsc_rays : bool, optional + Flag indicating whether to scatter rays from non-sequential sources at ray-surface intercepts. + Defaults to False. + draw_real_entrance_pupils : constants.Tools.Layouts.RealPupilOptions | str, optional + How to draw real entrance pupils. Defaults to "Pupils_Off". Can be one of ['Pupils_Off', 'Pupils_4', + 'Pupils_8', 'Pupils_16', 'Pupils_32']. + draw_real_exit_pupils : constants.Tools.Layouts.RealPupilOptions | str, optional + How to draw real exit pupils. Defaults to "Pupils_Off". Can be one of ['Pupils_Off', 'Pupils_4', 'Pupils_8', + 'Pupils_16', 'Pupils_32']. + surface_line_thickness : constants.Tools.Layouts.LineThicknessOptions | str, optional + The thickness of the lines for the surfaces. Defaults to "Standard". + rays_line_thickness : constants.Tools.Layouts.LineThicknessOptions | str, optional + The thickness of the lines for the rays. Defaults to "Standard". + configuration_all : bool, optional + Flag indicating whether to use all configurations, if multiple configurations are present. Defaults to False. + If multiple configurations are displayed, the `configuration_offset_*` parameters can be used to add offsets + between the different configurations. + configuration_current : bool, optional + Flag indicating whether to only display the current configuration, when multiple configurations are present. + Defaults to False. + configuration_offset_x : float, optional + The offset along the X-axis between configurations, if multiple configurations are present. Defaults to 0. + configuration_offset_y : float, optional + The offset along the Y-axis between configurations, if multiple configurations are present. Defaults to 0. + configuration_offset_z : float, optional + The offset along the Z-axis between configurations, if multiple configurations are present. Defaults to 0. + camera_viewpoint_angle_x : float, optional + Rotation of the system around the X-axis, in degrees. Defaults to 0. + camera_viewpoint_angle_y : float, optional + Rotation of the system around the Y-axis, in degrees. Defaults to 0. + camera_viewpoint_angle_z : float, optional + Rotation of the system around the Z-axis, in degrees. Defaults to 0. + image_size : tuple[int, int], optional + The size of the output image in pixels (width, height). Defaults to (800, 600). + """ + + start_surface: int = Field(default=1, ge=1, description="Starting surface number") + end_surface: Literal[-1] | Annotated[int, Field(ge=1)] = Field(default=-1, description="Ending surface number") + number_of_rays: int = Field(default=3, ge=1, description="Number of rays") + wavelength: WavelengthNumber = Field(default="All", description="Wavelength number") + field: FieldNumber = Field(default="All", description="Field number") + # ray_pattern: str = Field(default="XYFan", description="Ray pattern") + ray_pattern: ZOSAPIConstant("Tools.General.RayPatternType") = Field(default="XYFan", description="Ray pattern") + # color_rays_by: str = Field(default="Fields", description="Color rays by") + color_rays_by: ZOSAPIConstant("Tools.Layouts.ColorRaysByOptions") = Field( + default="Fields", description="Color rays by" + ) + delete_vignetted: bool = Field(default=False, description="Delete vignetted rays") + hide_lens_faces: bool = Field(default=False, description="Hide lens faces") + hide_lens_edges: bool = Field(default=False, description="Hide lens edges") + hide_x_bars: bool = Field(default=False, description="Hide X bars") + draw_paraxial_pupils: bool = Field(default=False, description="Draw paraxial pupils") + fletch_rays: bool = Field(default=False, description="Fletch rays") + split_nsc_rays: bool = Field(default=False, description="Split NSC rays") + scatter_nsc_rays: bool = Field(default=False, description="Scatter NSC rays") + # draw_real_entrance_pupils: str = Field(default="Pupils_Off", description="Draw real entrance pupils") + draw_real_entrance_pupils: ZOSAPIConstant("Tools.Layouts.RealPupilOptions") = Field( + default="Pupils_Off", description="Draw real entrance pupils" + ) + # draw_real_exit_pupils: str = Field(default="Pupils_Off", description="Draw real exit pupils") + draw_real_exit_pupils: ZOSAPIConstant("Tools.Layouts.RealPupilOptions") = Field( + default="Pupils_Off", description="Draw real exit pupils" + ) + # surface_line_thickness: str = Field(default="Standard", description="Surface line thickness") + surface_line_thickness: ZOSAPIConstant("Tools.Layouts.LineThicknessOptions") = Field( + default="Standard", description="Surface line thickness" + ) + # rays_line_thickness: str = Field(default="Standard", description="Rays line thickness") + rays_line_thickness: ZOSAPIConstant("Tools.Layouts.LineThicknessOptions") = Field( + default="Standard", description="Rays line thickness" + ) + configuration_all: bool = Field(default=False, description="Draw all configurations") + configuration_current: bool = Field(default=False, description="Draw only current configuration") + configuration_offset_x: float = Field(default=0, description="Configuration X offset") + configuration_offset_y: float = Field(default=0, description="Configuration Y offset") + configuration_offset_z: float = Field(default=0, description="Configuration Z offset") + camera_viewpoint_angle_x: float = Field(default=0, description="Camera viewpoint X angle") + camera_viewpoint_angle_y: float = Field(default=0, description="Camera viewpoint Y angle") + camera_viewpoint_angle_z: float = Field(default=0, description="Camera viewpoint Z angle") + image_size: ImageSize = Field(default=(800, 600), description="Image size") + + +class Viewer3D(SystemViewerWrapper[Viewer3DSettings]): + """3D system viewer.""" + + TYPE = "Draw3D" + MODE = "Sequential" + + def __init__( + self, + *, + start_surface: int = 1, + end_surface: int = -1, + number_of_rays: int = 3, + wavelength: int | Literal["All"] = "All", + field: int | Literal["All"] = "All", + ray_pattern: constants.Tools.General.RayPatternType | str = "XYFan", + color_rays_by: constants.Tools.Layouts.ColorRaysByOptions | str = "Fields", + delete_vignetted: bool = False, + hide_lens_faces: bool = False, + hide_lens_edges: bool = False, + hide_x_bars: bool = False, + draw_paraxial_pupils: bool = False, + fletch_rays: bool = False, + split_nsc_rays: bool = False, + scatter_nsc_rays: bool = False, + draw_real_entrance_pupils: constants.Tools.Layouts.RealPupilOptions | str = "Pupils_Off", + draw_real_exit_pupils: constants.Tools.Layouts.RealPupilOptions | str = "Pupils_Off", + surface_line_thickness: constants.Tools.Layouts.LineThicknessOptions | str = "Standard", + rays_line_thickness: constants.Tools.Layouts.LineThicknessOptions | str = "Standard", + configuration_all: bool = False, + configuration_current: bool = False, + configuration_offset_x: float = 0, + configuration_offset_y: float = 0, + configuration_offset_z: float = 0, + camera_viewpoint_angle_x: float = 0, + camera_viewpoint_angle_y: float = 0, + camera_viewpoint_angle_z: float = 0, + image_size: tuple[int, int] = (800, 600), + settings: Viewer3DSettings | None = None, + ): + super().__init__(settings or Viewer3DSettings(), locals()) + + def configure_layout_tool( + self, + ) -> _ZOSAPI.Tools.Layouts.I3DViewerExport: + """Configure the 3D viewer.""" + layout_tool = self.oss.Tools.Layouts.Open3DViewerExport() + + layout_tool.StartSurface = self.settings.start_surface + layout_tool.EndSurface = self._validate_end_surface(self.settings.start_surface, self.settings.end_surface) + layout_tool.NumberOfRays = self.settings.number_of_rays + layout_tool.Wavelength = self._validate_wavelength(self.settings.wavelength) + layout_tool.Field = self._validate_field(self.settings.field) + layout_tool.RayPattern = constants.process_constant( + constants.Tools.General.RayPatternType, self.settings.ray_pattern + ) + layout_tool.ColorRaysBy = constants.process_constant( + constants.Tools.Layouts.ColorRaysByOptions, self.settings.color_rays_by + ) + layout_tool.DeleteVignetted = self.settings.delete_vignetted + layout_tool.HideLensFaces = self.settings.hide_lens_faces + layout_tool.HideLensEdges = self.settings.hide_lens_edges + layout_tool.HideXBars = self.settings.hide_x_bars + layout_tool.DrawParaxialPupils = self.settings.draw_paraxial_pupils + layout_tool.FletchRays = self.settings.fletch_rays + layout_tool.SplitNSCRays = self.settings.split_nsc_rays + layout_tool.ScatterNSCRays = self.settings.scatter_nsc_rays + layout_tool.DrawRealEntrancePupils = constants.process_constant( + constants.Tools.Layouts.RealPupilOptions, self.settings.draw_real_entrance_pupils + ) + layout_tool.DrawRealExitPupils = constants.process_constant( + constants.Tools.Layouts.RealPupilOptions, self.settings.draw_real_exit_pupils + ) + layout_tool.SurfaceLineThickness = constants.process_constant( + constants.Tools.Layouts.LineThicknessOptions, self.settings.surface_line_thickness + ) + layout_tool.RaysLineThickness = constants.process_constant( + constants.Tools.Layouts.LineThicknessOptions, self.settings.rays_line_thickness + ) + layout_tool.ConfigurationAll = self.settings.configuration_all + layout_tool.ConfigurationCurrent = self.settings.configuration_current + layout_tool.ConfigurationOffsetX = self.settings.configuration_offset_x + layout_tool.ConfigurationOffsetY = self.settings.configuration_offset_y + layout_tool.ConfigurationOffsetZ = self.settings.configuration_offset_z + layout_tool.CameraViewpointAngleX = self.settings.camera_viewpoint_angle_x + layout_tool.CameraViewpointAngleY = self.settings.camera_viewpoint_angle_y + layout_tool.CameraViewpointAngleZ = self.settings.camera_viewpoint_angle_z + layout_tool.OutputPixelWidth, layout_tool.OutputPixelHeight = self.settings.image_size + + return layout_tool From 07a769bce262b46f5753a0fb0d00104e79ec73b8 Mon Sep 17 00:00:00 2001 From: crnh <30109443+crnh@users.noreply.github.com> Date: Thu, 12 Dec 2024 15:19:55 +0100 Subject: [PATCH 05/18] Add basic shaded model viewer --- zospy/analyses/new/systemviewers/__init__.py | 3 +- zospy/analyses/new/systemviewers/base.py | 8 +++- .../new/systemviewers/shaded_model.py | 46 +++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 zospy/analyses/new/systemviewers/shaded_model.py diff --git a/zospy/analyses/new/systemviewers/__init__.py b/zospy/analyses/new/systemviewers/__init__.py index a5fb3a8..2311fdf 100644 --- a/zospy/analyses/new/systemviewers/__init__.py +++ b/zospy/analyses/new/systemviewers/__init__.py @@ -1,6 +1,7 @@ """OpticStudio system viewers.""" from zospy.analyses.new.systemviewers.cross_section import CrossSection, CrossSectionSettings +from zospy.analyses.new.systemviewers.shaded_model import ShadedModel, ShadedModelSettings from zospy.analyses.new.systemviewers.viewer_3d import Viewer3D, Viewer3DSettings -__all__ = ("CrossSection", "CrossSectionSettings", "Viewer3D", "Viewer3DSettings") +__all__ = ("CrossSection", "CrossSectionSettings", "Viewer3D", "Viewer3DSettings", "ShadedModel", "ShadedModelSettings") diff --git a/zospy/analyses/new/systemviewers/base.py b/zospy/analyses/new/systemviewers/base.py index 0a63079..3f2ea30 100644 --- a/zospy/analyses/new/systemviewers/base.py +++ b/zospy/analyses/new/systemviewers/base.py @@ -125,7 +125,13 @@ def _get_image_data(image_data: _ZOSAPI.Tools.Layouts.IImageExportData | None) - @abstractmethod def configure_layout_tool( self, - ) -> _ZOSAPI.Tools.Layouts.ICrossSectionExport | _ZOSAPI.Tools.Layouts.I3DViewerExport: + ) -> ( + _ZOSAPI.Tools.Layouts.ICrossSectionExport + | _ZOSAPI.Tools.Layouts.I3DViewerExport + | _ZOSAPI.Tools.Layouts.IShadedModelExport + | _ZOSAPI.Tools.Layouts.INSC3DLayoutExport + | _ZOSAPI.Tools.Layouts.INSCShadedModelExport + ): """Configure the layout tool for the analysis.""" def run_analysis(self) -> np.ndarray | None: diff --git a/zospy/analyses/new/systemviewers/shaded_model.py b/zospy/analyses/new/systemviewers/shaded_model.py new file mode 100644 index 0000000..fa67d01 --- /dev/null +++ b/zospy/analyses/new/systemviewers/shaded_model.py @@ -0,0 +1,46 @@ +"""Shaded Model viewer.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pydantic import Field + +from zospy.analyses.new.decorators import analysis_settings +from zospy.analyses.new.systemviewers.base import ImageSize, SystemViewerWrapper + +if TYPE_CHECKING: + from zospy.api import _ZOSAPI + +__all__ = ("ShadedModel", "ShadedModelSettings") + + +@analysis_settings +class ShadedModelSettings: + """Settings for the Shaded Model viewer. + + Notes + ----- + Not all settings in OpticStudio are supported yet. + """ + + image_size: ImageSize = Field(default=(800, 600), description="Image size") + + +class ShadedModel(SystemViewerWrapper[ShadedModelSettings]): + """Shaded Model viewer.""" + + TYPE = "ShadedModel" + + def __init__(self, *, image_size: tuple[int, int] = (800, 600), settings: ShadedModelSettings | None = None): + super().__init__(settings or ShadedModelSettings, locals()) + + def configure_layout_tool( + self, + ) -> _ZOSAPI.Tools.Layouts.IShadedModelExport: + """Configure the shaded model viewer.""" + layout_tool = self.oss.Tools.Layouts.OpenShadedModelExport() + + layout_tool.OutputPixelWidth, layout_tool.OutputPixelHeight = self.settings.image_size + + return layout_tool From 26c9d5282469976af209740058e149eb60e38018 Mon Sep 17 00:00:00 2001 From: crnh <30109443+crnh@users.noreply.github.com> Date: Thu, 12 Dec 2024 15:41:06 +0100 Subject: [PATCH 06/18] Add basic NSC 3D Layout viewer --- zospy/analyses/new/systemviewers/__init__.py | 4 +- .../new/systemviewers/cross_section.py | 15 ++++-- .../new/systemviewers/nsc_3d_layout.py | 51 +++++++++++++++++++ .../new/systemviewers/shaded_model.py | 9 +++- zospy/analyses/new/systemviewers/viewer_3d.py | 13 ++++- 5 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 zospy/analyses/new/systemviewers/nsc_3d_layout.py diff --git a/zospy/analyses/new/systemviewers/__init__.py b/zospy/analyses/new/systemviewers/__init__.py index 2311fdf..90825cc 100644 --- a/zospy/analyses/new/systemviewers/__init__.py +++ b/zospy/analyses/new/systemviewers/__init__.py @@ -3,5 +3,7 @@ from zospy.analyses.new.systemviewers.cross_section import CrossSection, CrossSectionSettings from zospy.analyses.new.systemviewers.shaded_model import ShadedModel, ShadedModelSettings from zospy.analyses.new.systemviewers.viewer_3d import Viewer3D, Viewer3DSettings +from zospy.analyses.new.systemviewers.nsc_3d_layout import NSC3DLayout, NSC3DLayoutSettings -__all__ = ("CrossSection", "CrossSectionSettings", "Viewer3D", "Viewer3DSettings", "ShadedModel", "ShadedModelSettings") +__all__ = ("CrossSection", "CrossSectionSettings", "Viewer3D", "Viewer3DSettings", "ShadedModel", + "ShadedModelSettings", "NSC3DLayout", "NSC3DLayoutSettings") diff --git a/zospy/analyses/new/systemviewers/cross_section.py b/zospy/analyses/new/systemviewers/cross_section.py index 71b6104..4128fc5 100644 --- a/zospy/analyses/new/systemviewers/cross_section.py +++ b/zospy/analyses/new/systemviewers/cross_section.py @@ -2,9 +2,8 @@ from __future__ import annotations -from typing import Annotated, Literal +from typing import TYPE_CHECKING, Annotated, Literal -import numpy as np from pydantic.dataclasses import Field from zospy.analyses.new.decorators import analysis_settings @@ -12,6 +11,9 @@ from zospy.analyses.new.systemviewers.base import ImageSize, SystemViewerWrapper from zospy.api import constants +if TYPE_CHECKING: + from zospy.api import _ZOSAPI + __all__ = ("CrossSection", "CrossSectionSettings") @@ -103,10 +105,15 @@ def __init__( surface_line_thickness: constants.Tools.Layouts.LineThicknessOptions | str = "Standard", settings: CrossSectionSettings | None = None, ): - """Initialize the cross section viewer.""" + """Create a new cross section viewer. + + See Also + -------- + CrossSectionSettings : Settings for the cross section viewer. + """ super().__init__(settings or CrossSectionSettings(), locals()) - def configure_layout_tool(self) -> np.ndarray | None: + def configure_layout_tool(self) -> _ZOSAPI.Tools.Layouts.ICrossSectionExport: """Configure the cross section viewer.""" layout_tool = self.oss.Tools.Layouts.OpenCrossSectionExport() layout_tool.StartSurface = self.settings.start_surface diff --git a/zospy/analyses/new/systemviewers/nsc_3d_layout.py b/zospy/analyses/new/systemviewers/nsc_3d_layout.py new file mode 100644 index 0000000..eea5eec --- /dev/null +++ b/zospy/analyses/new/systemviewers/nsc_3d_layout.py @@ -0,0 +1,51 @@ +"""3D Layout viewer for Non-Sequential systems.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pydantic import Field + +from zospy.analyses.new.decorators import analysis_settings +from zospy.analyses.new.systemviewers.base import ImageSize, SystemViewerWrapper + +if TYPE_CHECKING: + from zospy.api import _ZOSAPI + + +@analysis_settings +class NSC3DLayoutSettings: + """Settings for the nonsequential 3D Layout viewer. + + Notes + ----- + Not all settings available in OpticStudio are supported yet. + """ + + image_size: ImageSize = Field(default=(800, 600), description="Image size") + + +class NSC3DLayout(SystemViewerWrapper[NSC3DLayoutSettings]): + """3D Layout viewer for Non-Sequential systems.""" + + TYPE = "NSC3DLayout" + MODE = "Nonsequential" + + def __init__(self, *, image_size: tuple[int, int] = (800, 600), settings: NSC3DLayoutSettings | None = None): + """Create a new nonsequential 3D Layout viewer. + + See Also + -------- + NSC3DLayoutSettings : Settings for the NSC 3D Layout viewer + """ + super().__init__(settings or NSC3DLayoutSettings(), locals()) + + def configure_layout_tool( + self, + ) -> _ZOSAPI.Tools.Layouts.INSC3DLayoutExport: + """Configure the nonsequential 3D Layout viewer.""" + layout_tool = self.oss.Tools.Layouts.OpenNSC3DLayoutExport() + + layout_tool.OutputPixelWidth, layout_tool.OutputPixelHeight = self.settings.image_size + + return layout_tool diff --git a/zospy/analyses/new/systemviewers/shaded_model.py b/zospy/analyses/new/systemviewers/shaded_model.py index fa67d01..6e554bc 100644 --- a/zospy/analyses/new/systemviewers/shaded_model.py +++ b/zospy/analyses/new/systemviewers/shaded_model.py @@ -21,7 +21,7 @@ class ShadedModelSettings: Notes ----- - Not all settings in OpticStudio are supported yet. + Not all settings available in OpticStudio are supported yet. """ image_size: ImageSize = Field(default=(800, 600), description="Image size") @@ -31,8 +31,15 @@ class ShadedModel(SystemViewerWrapper[ShadedModelSettings]): """Shaded Model viewer.""" TYPE = "ShadedModel" + MODE = "Sequential" def __init__(self, *, image_size: tuple[int, int] = (800, 600), settings: ShadedModelSettings | None = None): + """Initialize the Shaded Model viewer. + + See Also + -------- + ShadedModelSettings : Settings for the Shaded Model viewer. + """ super().__init__(settings or ShadedModelSettings, locals()) def configure_layout_tool( diff --git a/zospy/analyses/new/systemviewers/viewer_3d.py b/zospy/analyses/new/systemviewers/viewer_3d.py index 7d91270..3fda0a4 100644 --- a/zospy/analyses/new/systemviewers/viewer_3d.py +++ b/zospy/analyses/new/systemviewers/viewer_3d.py @@ -2,14 +2,17 @@ from __future__ import annotations -from typing import Annotated, Literal +from typing import TYPE_CHECKING, Annotated, Literal from pydantic import Field from zospy.analyses.new.decorators import analysis_settings from zospy.analyses.new.parsers.types import FieldNumber, WavelengthNumber, ZOSAPIConstant # noqa: TCH001 from zospy.analyses.new.systemviewers.base import ImageSize, SystemViewerWrapper -from zospy.api import _ZOSAPI, constants +from zospy.api import constants + +if TYPE_CHECKING: + from zospy.api import _ZOSAPI __all__ = ("Viewer3D", "Viewer3DSettings") @@ -169,6 +172,12 @@ def __init__( image_size: tuple[int, int] = (800, 600), settings: Viewer3DSettings | None = None, ): + """Create a new 3D system viewer. + + See Also + -------- + Viewer3DSettings : Settings for the 3D system viewer. + """ super().__init__(settings or Viewer3DSettings(), locals()) def configure_layout_tool( From 3a4c3d3ef3f020b1463e2d2b7bc8c6d68de39382 Mon Sep 17 00:00:00 2001 From: crnh <30109443+crnh@users.noreply.github.com> Date: Thu, 12 Dec 2024 15:49:45 +0100 Subject: [PATCH 07/18] Add basic NSC shaded model viewer --- zospy/analyses/new/systemviewers/__init__.py | 17 +++++-- .../new/systemviewers/nsc_shaded_model.py | 47 +++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 zospy/analyses/new/systemviewers/nsc_shaded_model.py diff --git a/zospy/analyses/new/systemviewers/__init__.py b/zospy/analyses/new/systemviewers/__init__.py index 90825cc..b53c729 100644 --- a/zospy/analyses/new/systemviewers/__init__.py +++ b/zospy/analyses/new/systemviewers/__init__.py @@ -1,9 +1,20 @@ """OpticStudio system viewers.""" from zospy.analyses.new.systemviewers.cross_section import CrossSection, CrossSectionSettings +from zospy.analyses.new.systemviewers.nsc_3d_layout import NSC3DLayout, NSC3DLayoutSettings +from zospy.analyses.new.systemviewers.nsc_shaded_model import NSCShadedModel, NSCShadedModelSettings from zospy.analyses.new.systemviewers.shaded_model import ShadedModel, ShadedModelSettings from zospy.analyses.new.systemviewers.viewer_3d import Viewer3D, Viewer3DSettings -from zospy.analyses.new.systemviewers.nsc_3d_layout import NSC3DLayout, NSC3DLayoutSettings -__all__ = ("CrossSection", "CrossSectionSettings", "Viewer3D", "Viewer3DSettings", "ShadedModel", - "ShadedModelSettings", "NSC3DLayout", "NSC3DLayoutSettings") +__all__ = ( + "CrossSection", + "CrossSectionSettings", + "Viewer3D", + "Viewer3DSettings", + "ShadedModel", + "ShadedModelSettings", + "NSC3DLayout", + "NSC3DLayoutSettings", + "NSCShadedModel", + "NSCShadedModelSettings", +) diff --git a/zospy/analyses/new/systemviewers/nsc_shaded_model.py b/zospy/analyses/new/systemviewers/nsc_shaded_model.py new file mode 100644 index 0000000..73e92fd --- /dev/null +++ b/zospy/analyses/new/systemviewers/nsc_shaded_model.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pydantic import Field + +from zospy.analyses.new.decorators import analysis_settings +from zospy.analyses.new.systemviewers.base import ImageSize, SystemViewerWrapper + +if TYPE_CHECKING: + from zospy.api import _ZOSAPI + + +@analysis_settings +class NSCShadedModelSettings: + """Settings for the nonsequential Shaded Model viewer. + + Notes + ----- + Not all settings available in OpticStudio are supported yet. + """ + + image_size: ImageSize = Field(default=(800, 600), description="Image size") + + +class NSCShadedModel(SystemViewerWrapper[NSCShadedModelSettings]): + """Nonsequential Shaded Model viewer.""" + + TYPE = "NSCShadedModel" + MODE = "Nonsequential" + + def __init__(self, *, image_size: ImageSize = (800, 600), settings: NSCShadedModelSettings | None = None): + """Create a new nonsequential shaded model viewer. + + See Also + -------- + NSCShadedModelSettings : Settings for the nonsequential shaded model viewer. + """ + super().__init__(settings or NSCShadedModelSettings(), locals()) + + def configure_layout_tool(self) -> _ZOSAPI.Tools.Layouts.IShadedModelExport: + """Configure the nonsequential shaded model viewer.""" + layout_tool = self.oss.Tools.Layouts.OpenShadedModelExport() + + layout_tool.OutputPixelWidth, layout_tool.OutputPixelHeight = self.settings.image_size + + return layout_tool From 633aecfef823ab20c3a7cf75d68f86e52f19e7bd Mon Sep 17 00:00:00 2001 From: crnh <30109443+crnh@users.noreply.github.com> Date: Thu, 12 Dec 2024 15:52:50 +0100 Subject: [PATCH 08/18] Format, lint --- zospy/analyses/new/systemviewers/base.py | 12 +++++++++--- zospy/analyses/new/systemviewers/cross_section.py | 2 +- zospy/analyses/new/systemviewers/nsc_3d_layout.py | 2 +- zospy/analyses/new/systemviewers/nsc_shaded_model.py | 2 ++ 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/zospy/analyses/new/systemviewers/base.py b/zospy/analyses/new/systemviewers/base.py index 3f2ea30..5995765 100644 --- a/zospy/analyses/new/systemviewers/base.py +++ b/zospy/analyses/new/systemviewers/base.py @@ -1,10 +1,14 @@ +"""Base classes for system viewers. + +System viewers are slightly different from other analyses: the analyses themselves are not configurable, +but there is a separate tool for each viewer. This module provides the base classes for interacting with these tools. +""" + from __future__ import annotations import weakref from abc import ABC, abstractmethod from dataclasses import fields -from os import PathLike -from pathlib import Path from typing import TYPE_CHECKING, Annotated, Generic, Literal from warnings import warn @@ -16,6 +20,9 @@ from zospy.utils.pyutils import abspath if TYPE_CHECKING: + from os import PathLike + from pathlib import Path + from zospy.api import _ZOSAPI from zospy.zpcore import OpticStudioSystem @@ -160,7 +167,6 @@ def run( oss: OpticStudioSystem, image_output_file: str | Path | None = None, oncomplete: OnComplete | Literal["Close", "Release", "Sustain"] = "Close", - **kwargs, ) -> AnalysisData: """Run the analysis. diff --git a/zospy/analyses/new/systemviewers/cross_section.py b/zospy/analyses/new/systemviewers/cross_section.py index 4128fc5..c8346db 100644 --- a/zospy/analyses/new/systemviewers/cross_section.py +++ b/zospy/analyses/new/systemviewers/cross_section.py @@ -7,7 +7,7 @@ from pydantic.dataclasses import Field from zospy.analyses.new.decorators import analysis_settings -from zospy.analyses.new.parsers.types import FieldNumber, WavelengthNumber, ZOSAPIConstant +from zospy.analyses.new.parsers.types import FieldNumber, WavelengthNumber, ZOSAPIConstant # noqa: TCH001 from zospy.analyses.new.systemviewers.base import ImageSize, SystemViewerWrapper from zospy.api import constants diff --git a/zospy/analyses/new/systemviewers/nsc_3d_layout.py b/zospy/analyses/new/systemviewers/nsc_3d_layout.py index eea5eec..b1014cf 100644 --- a/zospy/analyses/new/systemviewers/nsc_3d_layout.py +++ b/zospy/analyses/new/systemviewers/nsc_3d_layout.py @@ -1,4 +1,4 @@ -"""3D Layout viewer for Non-Sequential systems.""" +"""Nonsequential 3D Layout viewer.""" from __future__ import annotations diff --git a/zospy/analyses/new/systemviewers/nsc_shaded_model.py b/zospy/analyses/new/systemviewers/nsc_shaded_model.py index 73e92fd..b4dd88e 100644 --- a/zospy/analyses/new/systemviewers/nsc_shaded_model.py +++ b/zospy/analyses/new/systemviewers/nsc_shaded_model.py @@ -1,3 +1,5 @@ +"""Nonsequential Shaded Model viewer.""" + from __future__ import annotations from typing import TYPE_CHECKING From deec8d2d7cddf7c3a920cd3b0b96bb8ee9b1944a Mon Sep 17 00:00:00 2001 From: crnh <30109443+crnh@users.noreply.github.com> Date: Fri, 13 Dec 2024 08:58:07 +0100 Subject: [PATCH 09/18] Work on unit tests --- tests/analyses/new/test_base.py | 2 + tests/analyses/new/test_systemviewers.py | 51 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 tests/analyses/new/test_systemviewers.py diff --git a/tests/analyses/new/test_base.py b/tests/analyses/new/test_base.py index c405d9a..70f2c10 100644 --- a/tests/analyses/new/test_base.py +++ b/tests/analyses/new/test_base.py @@ -19,8 +19,10 @@ ) from zospy.analyses.new.parsers.types import ValidatedDataFrame from zospy.analyses.new.reports.surface_data import SurfaceDataSettings +from zospy.analyses.new.systemviewers.base import SystemViewerWrapper analysis_wrapper_classes = BaseAnalysisWrapper.__subclasses__() +analysis_wrapper_classes.remove(SystemViewerWrapper) @dataclass diff --git a/tests/analyses/new/test_systemviewers.py b/tests/analyses/new/test_systemviewers.py new file mode 100644 index 0000000..71eec0c --- /dev/null +++ b/tests/analyses/new/test_systemviewers.py @@ -0,0 +1,51 @@ +from zospy.analyses.new.systemviewers import CrossSection, NSC3DLayout, NSCShadedModel, ShadedModel, Viewer3D + + +class TestCrossSection: + def test_can_run(self, simple_system): + result = CrossSection().run(simple_system) + assert result.data is not None + + def test_to_json(self, simple_system): + result = CrossSection().run(simple_system) + assert result.from_json(result.to_json()).to_json() == result.to_json() + + +class TestViewer3D: + def test_can_run(self, simple_system): + result = Viewer3D().run(simple_system) + assert result.data is not None + + def test_to_json(self, simple_system): + result = Viewer3D().run(simple_system) + assert result.from_json(result.to_json()).to_json() == result.to_json() + + +class TestShadedModel: + def test_can_run(self, simple_system): + result = ShadedModel().run(simple_system) + assert result.data is not None + + def test_to_json(self, simple_system): + result = ShadedModel().run(simple_system) + assert result.from_json(result.to_json()).to_json() == result.to_json() + + +class TestNSC3DLayout: + def test_can_run(self, nsc_simple_system): + result = NSC3DLayout().run(nsc_simple_system) + assert result.data is not None + + def test_to_json(self, nsc_simple_system): + result = NSC3DLayout().run(nsc_simple_system) + assert result.from_json(result.to_json()).to_json() == result.to_json() + + +class TestNSCShadedModel: + def test_can_run(self, nsc_simple_system): + result = NSCShadedModel().run(nsc_simple_system) + assert result.data is not None + + def test_to_json(self, nsc_simple_system): + result = NSCShadedModel().run(nsc_simple_system) + assert result.from_json(result.to_json()).to_json() == result.to_json() From 40568c6ddb202c76d24bad79ffb3d80a4c7ff5f6 Mon Sep 17 00:00:00 2001 From: crnh <30109443+crnh@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:39:02 +0100 Subject: [PATCH 10/18] Add tests for system viewers and base --- tests/analyses/new/test_systemviewers.py | 151 +++++++++++++++++++++++ zospy/analyses/new/systemviewers/base.py | 10 +- 2 files changed, 159 insertions(+), 2 deletions(-) diff --git a/tests/analyses/new/test_systemviewers.py b/tests/analyses/new/test_systemviewers.py index 71eec0c..947718e 100644 --- a/tests/analyses/new/test_systemviewers.py +++ b/tests/analyses/new/test_systemviewers.py @@ -1,4 +1,155 @@ +from contextlib import nullcontext as does_not_raise +from datetime import datetime +from types import SimpleNamespace + +import pytest + +from zospy.analyses.new.base import AnalysisMetadata from zospy.analyses.new.systemviewers import CrossSection, NSC3DLayout, NSCShadedModel, ShadedModel, Viewer3D +from zospy.analyses.new.systemviewers.base import SystemViewerWrapper + + +class TestBase: + class MockSystemViewer(SystemViewerWrapper[None]): + def __init__(self): + super().__init__(None, {}) + + def _create_analysis(self, *, settings_first=True): # noqa: ARG002 + self._analysis = SimpleNamespace( + metadata=AnalysisMetadata(DateTime=datetime.now(), LensFile="", LensTitle="", FeatureDescription=""), + header_data=None, + messages=[], + Close=lambda: None, + ) + + def _do_nothing(self): + pass + + def configure_layout_tool(self): + pass + + def run_analysis(self) -> None: + return None + + @pytest.mark.parametrize( + "filename,expectation", + [ + ("test.bmp", does_not_raise()), + ("test.jpeg", does_not_raise()), + ("test.png", does_not_raise()), + ("test.jpg", pytest.raises(ValueError, match="Image file must have one of the following extensions:")), + ("test.tiff", pytest.raises(ValueError, match="Image file must have one of the following extensions:")), + ], + ) + def test_validate_path(self, filename, expectation, tmp_path): + viewer = TestBase.MockSystemViewer() + + with expectation: + result = viewer._validate_path(tmp_path / filename) # noqa: SLF001 + assert result == str(tmp_path / filename) + + @pytest.mark.parametrize( + "wavelength,expected,expectation", + [ + (1, 1, does_not_raise()), + (4, 4, does_not_raise()), + (-1, -1, does_not_raise()), + ("All", -1, does_not_raise()), + ("a", None, pytest.raises(ValueError, match="wavelength must be an integer or 'All'")), + (0.55, None, pytest.raises(TypeError, match="wavelength must be an integer or 'All'")), + ( + -2, + None, + pytest.raises(ValueError, match="wavelength must be -1 or between 1 and the number of wavelengths"), + ), + ( + -0, + None, + pytest.raises(ValueError, match="wavelength must be -1 or between 1 and the number of wavelengths"), + ), + ( + 5, + None, + pytest.raises(ValueError, match="wavelength must be -1 or between 1 and the number of wavelengths"), + ), + ], + ) + def test_validate_wavelength(self, wavelength, expected, expectation, simple_system): + while simple_system.SystemData.Wavelengths.NumberOfWavelengths < 4: + simple_system.SystemData.Wavelengths.AddWavelength(0.555, 1) + + viewer = TestBase.MockSystemViewer() + viewer.run(simple_system) + + with expectation: + result = viewer._validate_wavelength(wavelength) # noqa: SLF001 + assert result == expected + + @pytest.mark.parametrize( + "field,expected,expectation", + [ + (1, 1, does_not_raise()), + (4, 4, does_not_raise()), + (-1, -1, does_not_raise()), + ("All", -1, does_not_raise()), + ("a", None, pytest.raises(ValueError, match="field must be an integer or 'All'")), + (0.55, None, pytest.raises(TypeError, match="field must be an integer or 'All'")), + ( + -2, + None, + pytest.raises(ValueError, match="field must be -1 or between 1 and the number of fields"), + ), + (0, None, pytest.raises(ValueError, match="field must be -1 or between 1 and the number of fields")), + ( + 5, + None, + pytest.raises(ValueError, match="field must be -1 or between 1 and the number of fields"), + ), + ], + ) + def test_validate_field(self, field, expected, expectation, simple_system): + while simple_system.SystemData.Fields.NumberOfFields < 4: + simple_system.SystemData.Fields.AddField(1, 1, 1) + + viewer = TestBase.MockSystemViewer() + viewer.run(simple_system) + + with expectation: + result = viewer._validate_field(field) # noqa: SLF001 + assert result == expected + + @pytest.mark.parametrize( + "start_surface,end_surface,expected,expectation", + [ + (1, 2, 2, does_not_raise()), + (1, -1, 4, does_not_raise()), + ( + 1, + 5, + None, + pytest.raises( + ValueError, + match="end_surface must be -1 or greater than start_surface and less than the number of surfaces", + ), + ), + ( + 3, + 1, + None, + pytest.raises( + ValueError, + match="end_surface must be -1 or greater than start_surface and less than the number of surfaces", + ), + ), + ], + ) + def test_validate_end_surface(self, start_surface, end_surface, expected, expectation, simple_system): + viewer = TestBase.MockSystemViewer() + viewer.run(simple_system) + + with expectation: + result = viewer._validate_end_surface(start_surface, end_surface) # noqa: SLF001 + assert result == expected class TestCrossSection: diff --git a/zospy/analyses/new/systemviewers/base.py b/zospy/analyses/new/systemviewers/base.py index 5995765..965e343 100644 --- a/zospy/analyses/new/systemviewers/base.py +++ b/zospy/analyses/new/systemviewers/base.py @@ -80,9 +80,12 @@ def _validate_wavelength(self, wavelength: int | str) -> int: raise ValueError("wavelength must be an integer or 'All'.") - if wavelength < -1 or wavelength > self.oss.SystemData.Wavelengths.NumberOfWavelengths: + if wavelength < -1 or wavelength == 0 or wavelength > self.oss.SystemData.Wavelengths.NumberOfWavelengths: raise ValueError("wavelength must be -1 or between 1 and the number of wavelengths.") + if not isinstance(wavelength, int): + raise TypeError("wavelength must be an integer or 'All'.") + return wavelength def _validate_field(self, field: int | str) -> int: @@ -92,9 +95,12 @@ def _validate_field(self, field: int | str) -> int: raise ValueError("field must be an integer or 'All'.") - if field < 1 or field > self.oss.SystemData.Fields.NumberOfFields: + if field < -1 or field == 0 or field > self.oss.SystemData.Fields.NumberOfFields: raise ValueError("field must be -1 or between 1 and the number of fields.") + if not isinstance(field, int): + raise TypeError("field must be an integer or 'All'.") + return field def _validate_end_surface(self, start_surface, end_surface: int): From 4e2e59cf392c1aa74c9ab99916f50d4c77352b89 Mon Sep 17 00:00:00 2001 From: crnh <30109443+crnh@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:54:32 +0100 Subject: [PATCH 11/18] Use settings instance instead of class --- zospy/analyses/new/systemviewers/shaded_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zospy/analyses/new/systemviewers/shaded_model.py b/zospy/analyses/new/systemviewers/shaded_model.py index 6e554bc..85ee20e 100644 --- a/zospy/analyses/new/systemviewers/shaded_model.py +++ b/zospy/analyses/new/systemviewers/shaded_model.py @@ -40,7 +40,7 @@ def __init__(self, *, image_size: tuple[int, int] = (800, 600), settings: Shaded -------- ShadedModelSettings : Settings for the Shaded Model viewer. """ - super().__init__(settings or ShadedModelSettings, locals()) + super().__init__(settings or ShadedModelSettings(), locals()) def configure_layout_tool( self, From 3232a2b40d10d3ffcfdd46f5d7c97115ec41e13b Mon Sep 17 00:00:00 2001 From: crnh <30109443+crnh@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:01:34 +0100 Subject: [PATCH 12/18] Add test for ignored settings --- tests/analyses/new/test_systemviewers.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/tests/analyses/new/test_systemviewers.py b/tests/analyses/new/test_systemviewers.py index 947718e..96c12b1 100644 --- a/tests/analyses/new/test_systemviewers.py +++ b/tests/analyses/new/test_systemviewers.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from contextlib import nullcontext as does_not_raise from datetime import datetime from types import SimpleNamespace @@ -5,14 +7,19 @@ import pytest from zospy.analyses.new.base import AnalysisMetadata +from zospy.analyses.new.decorators import analysis_settings from zospy.analyses.new.systemviewers import CrossSection, NSC3DLayout, NSCShadedModel, ShadedModel, Viewer3D from zospy.analyses.new.systemviewers.base import SystemViewerWrapper class TestBase: - class MockSystemViewer(SystemViewerWrapper[None]): - def __init__(self): - super().__init__(None, {}) + @analysis_settings + class MockSystemViewerSettings: + number: int = 5 + + class MockSystemViewer(SystemViewerWrapper[MockSystemViewerSettings]): + def __init__(self, *, number: int = 5, settings: TestBase.MockSystemViewerSettings | None = None): + super().__init__(settings or TestBase.MockSystemViewerSettings(), locals()) def _create_analysis(self, *, settings_first=True): # noqa: ARG002 self._analysis = SimpleNamespace( @@ -151,6 +158,13 @@ def test_validate_end_surface(self, start_surface, end_surface, expected, expect result = viewer._validate_end_surface(start_surface, end_surface) # noqa: SLF001 assert result == expected + @pytest.mark.skip_for_opticstudio_versions(">=24.1.0", "Settings are supported from OpticStudio 24R1") + def test_warn_ignored_settings(self): + viewer = TestBase.MockSystemViewer(number=6) + + with pytest.warns(UserWarning, match="Some parameters were specified but ignored"): + viewer.run() + class TestCrossSection: def test_can_run(self, simple_system): From abef1839b91f208c9f096228711497fe3760ee73 Mon Sep 17 00:00:00 2001 From: crnh <30109443+crnh@users.noreply.github.com> Date: Mon, 16 Dec 2024 08:33:27 +0100 Subject: [PATCH 13/18] Add missing system to test --- tests/analyses/new/test_systemviewers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/analyses/new/test_systemviewers.py b/tests/analyses/new/test_systemviewers.py index 96c12b1..ad48d25 100644 --- a/tests/analyses/new/test_systemviewers.py +++ b/tests/analyses/new/test_systemviewers.py @@ -159,11 +159,11 @@ def test_validate_end_surface(self, start_surface, end_surface, expected, expect assert result == expected @pytest.mark.skip_for_opticstudio_versions(">=24.1.0", "Settings are supported from OpticStudio 24R1") - def test_warn_ignored_settings(self): + def test_warn_ignored_settings(self, simple_system): viewer = TestBase.MockSystemViewer(number=6) with pytest.warns(UserWarning, match="Some parameters were specified but ignored"): - viewer.run() + viewer.run(simple_system) class TestCrossSection: From 44a124977e5474c88694844ffcb4882741adb542 Mon Sep 17 00:00:00 2001 From: lvanvught Date: Mon, 16 Dec 2024 14:17:34 +0100 Subject: [PATCH 14/18] Ensured AnalysisData can also be None --- zospy/analyses/new/base.py | 8 +++++++- zospy/analyses/new/parsers/types.py | 6 ++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/zospy/analyses/new/base.py b/zospy/analyses/new/base.py index 9e925f8..cdf0e64 100644 --- a/zospy/analyses/new/base.py +++ b/zospy/analyses/new/base.py @@ -94,12 +94,15 @@ class AnalysisMetadata: class _TypeInfo(TypedDict): - data_type: Literal["dataframe", "ndarray", "dataclass"] + data_type: Literal["dataframe", "ndarray", "dataclass", "none"] name: NotRequired[str | None] module: NotRequired[str | None] def _serialize_analysis_data_type(data: AnalysisData) -> _TypeInfo: + if data is None: + return {"data_type": "none"} + if isinstance(data, pd.DataFrame): return {"data_type": "dataframe"} @@ -126,6 +129,9 @@ def _deserialize_dataclass(data: dict, typeinfo: _TypeInfo) -> AnalysisData: def _deserialize_analysis_data(data: dict | list, typeinfo: _TypeInfo) -> AnalysisData: + if typeinfo['data_type'] == "none": + return None + if typeinfo["data_type"] == "dataframe": return pd.DataFrame.from_dict(data, orient="tight") diff --git a/zospy/analyses/new/parsers/types.py b/zospy/analyses/new/parsers/types.py index 2b2fc90..680bcdd 100644 --- a/zospy/analyses/new/parsers/types.py +++ b/zospy/analyses/new/parsers/types.py @@ -2,6 +2,7 @@ from __future__ import annotations +import logging from operator import attrgetter from typing import TYPE_CHECKING, Annotated, Any, Generic, Literal, TypeVar, Union @@ -130,8 +131,9 @@ def __init__(self, enum: str): def _validate_constant(self, value: ZospyConstantType | str) -> ZospyConstantType: try: constant = attrgetter(self.enum)(constants) - except AttributeError as e: - raise AttributeError(f"Constant {self.enum} not found in zospy.constants") from e + except AttributeError: + logging.warning(f"Constant {self.enum} not found in zospy.constants") + return None return constants.process_constant(constant, value) From 540cd78a73d86b1fa8147be9ba6be08e2436a569 Mon Sep 17 00:00:00 2001 From: lvanvught Date: Mon, 16 Dec 2024 14:18:29 +0100 Subject: [PATCH 15/18] Corrected SystemViewer tests for older OpticStudio versions --- tests/analyses/new/test_systemviewers.py | 27 +++++++++++++++--------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/tests/analyses/new/test_systemviewers.py b/tests/analyses/new/test_systemviewers.py index ad48d25..f837fca 100644 --- a/tests/analyses/new/test_systemviewers.py +++ b/tests/analyses/new/test_systemviewers.py @@ -11,6 +11,13 @@ from zospy.analyses.new.systemviewers import CrossSection, NSC3DLayout, NSCShadedModel, ShadedModel, Viewer3D from zospy.analyses.new.systemviewers.base import SystemViewerWrapper +def consider_minimal_opticstudio_version_in_result(result, minimal_version): + """Makes sure systemviewer results is correctly asserted for different versions of OpticStudio. + """ + if minimal_version >= '24.1.0': + assert result.data is not None + else: # No result.data as layout exports are not supported + assert result.data is None class TestBase: @analysis_settings @@ -167,9 +174,9 @@ def test_warn_ignored_settings(self, simple_system): class TestCrossSection: - def test_can_run(self, simple_system): + def test_can_run(self, simple_system, optic_studio_version): result = CrossSection().run(simple_system) - assert result.data is not None + consider_minimal_opticstudio_version_in_result(result, optic_studio_version) def test_to_json(self, simple_system): result = CrossSection().run(simple_system) @@ -177,9 +184,9 @@ def test_to_json(self, simple_system): class TestViewer3D: - def test_can_run(self, simple_system): + def test_can_run(self, simple_system, optic_studio_version): result = Viewer3D().run(simple_system) - assert result.data is not None + consider_minimal_opticstudio_version_in_result(result, optic_studio_version) def test_to_json(self, simple_system): result = Viewer3D().run(simple_system) @@ -187,9 +194,9 @@ def test_to_json(self, simple_system): class TestShadedModel: - def test_can_run(self, simple_system): + def test_can_run(self, simple_system, optic_studio_version): result = ShadedModel().run(simple_system) - assert result.data is not None + consider_minimal_opticstudio_version_in_result(result, optic_studio_version) def test_to_json(self, simple_system): result = ShadedModel().run(simple_system) @@ -197,9 +204,9 @@ def test_to_json(self, simple_system): class TestNSC3DLayout: - def test_can_run(self, nsc_simple_system): + def test_can_run(self, nsc_simple_system, optic_studio_version): result = NSC3DLayout().run(nsc_simple_system) - assert result.data is not None + consider_minimal_opticstudio_version_in_result(result, optic_studio_version) def test_to_json(self, nsc_simple_system): result = NSC3DLayout().run(nsc_simple_system) @@ -207,9 +214,9 @@ def test_to_json(self, nsc_simple_system): class TestNSCShadedModel: - def test_can_run(self, nsc_simple_system): + def test_can_run(self, nsc_simple_system, optic_studio_version): result = NSCShadedModel().run(nsc_simple_system) - assert result.data is not None + consider_minimal_opticstudio_version_in_result(result, optic_studio_version) def test_to_json(self, nsc_simple_system): result = NSCShadedModel().run(nsc_simple_system) From 39407aa390be1f3619b674a0b438e2b7ec7cf113 Mon Sep 17 00:00:00 2001 From: lvanvught Date: Mon, 16 Dec 2024 14:19:56 +0100 Subject: [PATCH 16/18] format, lint --- tests/analyses/new/test_systemviewers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/analyses/new/test_systemviewers.py b/tests/analyses/new/test_systemviewers.py index f837fca..afacd11 100644 --- a/tests/analyses/new/test_systemviewers.py +++ b/tests/analyses/new/test_systemviewers.py @@ -11,9 +11,9 @@ from zospy.analyses.new.systemviewers import CrossSection, NSC3DLayout, NSCShadedModel, ShadedModel, Viewer3D from zospy.analyses.new.systemviewers.base import SystemViewerWrapper + def consider_minimal_opticstudio_version_in_result(result, minimal_version): - """Makes sure systemviewer results is correctly asserted for different versions of OpticStudio. - """ + """Makes sure systemviewer results is correctly asserted for different versions of OpticStudio.""" if minimal_version >= '24.1.0': assert result.data is not None else: # No result.data as layout exports are not supported From 6a5976f7867b52725f4695826d69562664ac68bf Mon Sep 17 00:00:00 2001 From: lvanvught Date: Tue, 17 Dec 2024 11:22:09 +0100 Subject: [PATCH 17/18] format,lint --- tests/analyses/new/test_systemviewers.py | 3 ++- zospy/analyses/new/base.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/analyses/new/test_systemviewers.py b/tests/analyses/new/test_systemviewers.py index afacd11..1eb128c 100644 --- a/tests/analyses/new/test_systemviewers.py +++ b/tests/analyses/new/test_systemviewers.py @@ -14,11 +14,12 @@ def consider_minimal_opticstudio_version_in_result(result, minimal_version): """Makes sure systemviewer results is correctly asserted for different versions of OpticStudio.""" - if minimal_version >= '24.1.0': + if minimal_version >= "24.1.0": assert result.data is not None else: # No result.data as layout exports are not supported assert result.data is None + class TestBase: @analysis_settings class MockSystemViewerSettings: diff --git a/zospy/analyses/new/base.py b/zospy/analyses/new/base.py index cdf0e64..ee83c14 100644 --- a/zospy/analyses/new/base.py +++ b/zospy/analyses/new/base.py @@ -129,7 +129,7 @@ def _deserialize_dataclass(data: dict, typeinfo: _TypeInfo) -> AnalysisData: def _deserialize_analysis_data(data: dict | list, typeinfo: _TypeInfo) -> AnalysisData: - if typeinfo['data_type'] == "none": + if typeinfo["data_type"] == "none": return None if typeinfo["data_type"] == "dataframe": From bec5a22fc9e6d4147f6b8700f577ddccee6d3961 Mon Sep 17 00:00:00 2001 From: lvanvught Date: Wed, 18 Dec 2024 11:06:12 +0100 Subject: [PATCH 18/18] changed function name to assert_systemviewer_result --- tests/analyses/new/test_systemviewers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/analyses/new/test_systemviewers.py b/tests/analyses/new/test_systemviewers.py index 1eb128c..a4bbae5 100644 --- a/tests/analyses/new/test_systemviewers.py +++ b/tests/analyses/new/test_systemviewers.py @@ -12,7 +12,7 @@ from zospy.analyses.new.systemviewers.base import SystemViewerWrapper -def consider_minimal_opticstudio_version_in_result(result, minimal_version): +def assert_systemviewer_result(result, minimal_version): """Makes sure systemviewer results is correctly asserted for different versions of OpticStudio.""" if minimal_version >= "24.1.0": assert result.data is not None @@ -177,7 +177,7 @@ def test_warn_ignored_settings(self, simple_system): class TestCrossSection: def test_can_run(self, simple_system, optic_studio_version): result = CrossSection().run(simple_system) - consider_minimal_opticstudio_version_in_result(result, optic_studio_version) + assert_systemviewer_result(result, optic_studio_version) def test_to_json(self, simple_system): result = CrossSection().run(simple_system) @@ -187,7 +187,7 @@ def test_to_json(self, simple_system): class TestViewer3D: def test_can_run(self, simple_system, optic_studio_version): result = Viewer3D().run(simple_system) - consider_minimal_opticstudio_version_in_result(result, optic_studio_version) + assert_systemviewer_result(result, optic_studio_version) def test_to_json(self, simple_system): result = Viewer3D().run(simple_system) @@ -197,7 +197,7 @@ def test_to_json(self, simple_system): class TestShadedModel: def test_can_run(self, simple_system, optic_studio_version): result = ShadedModel().run(simple_system) - consider_minimal_opticstudio_version_in_result(result, optic_studio_version) + assert_systemviewer_result(result, optic_studio_version) def test_to_json(self, simple_system): result = ShadedModel().run(simple_system) @@ -207,7 +207,7 @@ def test_to_json(self, simple_system): class TestNSC3DLayout: def test_can_run(self, nsc_simple_system, optic_studio_version): result = NSC3DLayout().run(nsc_simple_system) - consider_minimal_opticstudio_version_in_result(result, optic_studio_version) + assert_systemviewer_result(result, optic_studio_version) def test_to_json(self, nsc_simple_system): result = NSC3DLayout().run(nsc_simple_system) @@ -217,7 +217,7 @@ def test_to_json(self, nsc_simple_system): class TestNSCShadedModel: def test_can_run(self, nsc_simple_system, optic_studio_version): result = NSCShadedModel().run(nsc_simple_system) - consider_minimal_opticstudio_version_in_result(result, optic_studio_version) + assert_systemviewer_result(result, optic_studio_version) def test_to_json(self, nsc_simple_system): result = NSCShadedModel().run(nsc_simple_system)