# Python Library for Simulation of Optical Systems in VirtualLab Fusion

## Introduction

This library allows users to run optical systems from VirtualLab Fusion directly from a Python script. The results then can be processed with all capabilities that Python offers.

## Preparation

### VirtualLab Fusion

1. Create the optical setup in VirtualLab Fusion.
2. Save the optical setup in your desired location. For a simpler handling, save the os file in the "resources" folder of this package.
3. Export the parameters of the optical setup for accessing the name of a parameter, to later manipulate its value in a parameter run or scan. This works the following way:  
`File > Export > Export as XML`  

### Python

1. Make sure to have a Python 3.8 (or older) installation on your computer. Newer Python distributions are not supported by the Python.NET package.
2. Open your Python command line and change directory to the "SampleFiles" folder.
3. In order to install all required packages, run the following command  
`pip install -r requirements.txt`

### Configuration

1. Open the config.ini file.
2. Set the directory of your VL installation.  
```ini
[paths]
virtuallab = C:\Program Files\Wyrowski Photonics GmbH\VirtualLab Fusion 2023
```

## Package Content

### vlfpy.objects

#### Enum Physical Property

This enumeration determines what the physical property of a physical value is, e.g. length, time, percentage etc.

#### Abstract Class PhysicalValueBase

Base class for storage of physical values.

##### Attributes

- `physical_property: PhysicalProperty` the physical property of the physical value
- `comment: str` a comment specifying what the physical value actually is

#### Class PhysicalValue

Class for storage of _real_ physical values. Inherits from `PhysicalValueBase`.

##### Attributes

- `value: float` the numerical value of the physical value
- `physical_property: PhysicalProperty` the physical property of the physical value
- `comment: str` a comment specifying what the physical value actually is

The numerical value needs to be set in the base unit for the according physical property, e.g. length in [m].

#### Class PhysicalValueComplex

Class for storage of _complex_ physical values. Inherits from `PhysicalValueBase`.

##### Attributes

- `value: Complex` the numerical value of the physical value
- `physical_property: PhysicalProperty` the physical property of the physical value
- `comment: str` a comment specifying what the physical value actually is

The numerical value needs to be set in the base unit for the according physical property, e.g. length in [m]. The `Complex` data type is a VL data type. The full name is `VirtualLabAPI.Core.Numerics.Complex`.

#### Class Document

Class for storage of VL IDocument data types, e.g. DataArray2D.

##### Methods

- `save(path: str, file_name: str)` saves the Document
  - `path` : location
  - `file_name` : name of the file w/o extension; extension is determined automatically

### vlfpy.simulation

#### Class ParameterSet

Class for parameter definition for parameter runs / scans.

##### Example of XML Export

```xml
...
<LightPathElement Index="1" Name="Rectangular Grating">
    ...
    <Parameter>
        <Name>Stack #2 (Rectangular Grating) | Surface #1 (Rectangular Grating Interface) | Modulation Depth</Name>
        <ID>Stack2.LayersAsArray[0].Interface.ModulationDepth</ID>
        <ShortName>Modulation Depth</ShortName>
        <Value>1.8500000000000001e-06</Value>
        <Unit>m</Unit>
        <Min>1E-13</Min>
        <Max>1.0000000000000001E+300</Max>
    </Parameter>
    ...
</LightPathElement>
...
```

##### Attributes

- `lpe_index: int` index of the LightPathElement (_1_ for _Rectangular Grating_ in XML)
- `param_name: str` name (**ID**) of the Parameter (_Stack2.LayersAsArray[0].Interface.ModulationDepth_ in XML)
- `values: List[float]` the values to be varied in parameter run, in XML:
  - current value: _1.8500000000000001e-06_
  - base unit: _m_
  - min value: _1E-13_
  - max value: _1.0000000000000001E+300_

So in this example the value for the Modulation Depth has to be passed in [m] and has to be in the range of min & max values.

#### Class DetectorResultCollection

Class holding the results of a detector.

##### Attributes

- `description: str` the name of the detector
- `data: List[List[PhysicalValueBase] or Document]` the results, each entry in the list is the result for one run

#### Class OpticalSetup

Class holding the optical setup.

##### Methods

- `change_parameter(lpe_index: int, param_name: str, value: float)` changes a parameter of the optical system
  - `lpe_index: int` index of the LightPathElement
  - `param_name: str` name (ID) of the parameter
  - `value: float` the numerical value of the parameter
  - for further information check the documentation of the ParameterSet class
- `perform(results: List[DetectorResultCollection] = None) -> List[DetectorResultCollection]` simulates the optical setup
  - `results: List[DetectorResultCollection]` optional parameter, if passed the results will be extended
  - **returns** `List[DetectorResultCollection]`

#### Function for 1D Parameter Scan

`parameter_scan_1D()`

##### Arguments

- `optical_setup: OpticalSetup` the optical setup to be scanned
- `params: ParameterSet` the information for the parameter variation

##### Returns

- `List[DetectorResultCollection]` the results of the parameter scan for each detector

#### Function for 2D Parameter Scan

`parameter_scan_2D()`

##### Arguments

- `optical_setup: OpticalSetup` the optical setup to be scanned
- `params_1: ParameterSet` the information for the parameter variation of the first parameter
- `params_2: ParameterSet` the information for the parameter variation of the second parameter

##### Returns

- `List[DetectorResultCollection]` the results of the parameter scan for each detector

## Simulation in main.py

### Set the Path to the Optical Setup

```python
OS_PATH = r'resources\GratingEfficiency.os'
```

Here only one optical setup will be used for demonstration, thus the path is defined as a constant. The path can be either set relative or absolute. For relative paths it's recommended to use the _resources_ folder for storing the os files.

### Predefined Simulation Methods

There are 3 predefined methods for simulation. All of them print the results and the simulation time to the console. The parameter scans also provide plotting using the module _matplotlib.pyplot_.

- `single_run()` simulates the optical setup
- `parameter_scanning_1D()` performs a 1D parameter scan varying the _modulation depth_ of the _rectangular grating_
- `parameter_scanning_2D()` performs a 2D parameter scan varying the _modulation depth_ and the _relative slit width_ of the _rectangular grating_

### Run the Simulation

```python
if __name__ == '__main__':
    #single_run()
    parameter_scanning_1D()
    #parameter_scanning_2D()
```

Simply uncomment one of the methods and run the main.py file. Here the 1D parameter scan will be performed.  
