Download this example
Download this example as a Jupyter Notebook or as a Python script. All assets used in the examples can be downloaded as a ZIP archive.
How to create Speos input files#
This tutorial demonstrates how to create the files that Speos takes as inputs: materials, surface optical properties, spectra, ray files and 3D texture mappings.
What are Speos input files#
Speos reads a handful of documented file formats to describe optical properties, spectral distributions, measured light sources and textured surfaces. They are usually authored in the Speos editors, but they can also be generated from raw measurement data or from another simulation tool.
PySpeos models each of these formats with a class exposing the same three methods:
save(path)writes the model to a file,load(path)reads a file back into a model,validate()checks the model against the constraints of the format.
Unlike the rest of PySpeos, these classes work entirely offline: no connection to a Speos RPC server is needed. Once the files are written, they are handed over to the features that consume them, through their file URI.
Prerequisites#
Perform imports#
[1]:
from pathlib import Path
import tempfile
from ansys.speos.core import (
CoatedSurfaceFile,
CoatedSurfaceSample,
FluorescentPigment,
FluorescentPigmentFile,
MaterialConstringence,
MaterialFile,
MaterialType,
Ray,
RayFile,
ScatteringSurfaceFile,
ScatteringSurfaceSample,
SimpleScatteringMode,
SimpleScatteringSurfaceFile,
SpectrumFile,
Texture3DMappingFile,
TexturePattern,
VolumeScatteringHenyeyGreenstein,
)
[2]:
# ### Define constants
# Every file created by this example is written into a temporary directory.
OUTPUT_DIR = Path(tempfile.mkdtemp(prefix="pyspeos_file_formats_"))
Create a spectrum file#
A *.spectrum file is a sampled spectral distribution, used by most source types and by the camera sensors. The wavelengths are given in nm and the values in percent.
[3]:
spectrum = SpectrumFile(
description="Amber LED",
wavelengths=[560.0, 580.0, 600.0, 620.0, 640.0],
values=[5.0, 45.0, 100.0, 55.0, 10.0],
)
spectrum_path = spectrum.save(OUTPUT_DIR / "amber_led.spectrum")
print(spectrum_path.read_text())
OPTIS - Spectrum file v1
Amber LED
5
560 5
580 45
600 100
620 55
640 10
Reading the file back gives an equivalent model, which makes it easy to adjust a file that already exists.
[4]:
reloaded = SpectrumFile.load(spectrum_path)
print(reloaded.wavelengths, reloaded.values)
[560.0, 580.0, 600.0, 620.0, 640.0] [5.0, 45.0, 100.0, 55.0, 10.0]
Property assignments validate immediately. List getters return copies, so assign an edited list back to update the spectrum. validate() and save() require complete, matching data.
[5]:
values = reloaded.values
values[0] = 10.0
reloaded.values = values
print(reloaded.values)
[10.0, 45.0, 100.0, 55.0, 10.0]
Called without arguments, every file constructor creates a useful default. For a spectrum, this is a 5200 K blackbody.
[6]:
default_spectrum = SpectrumFile()
print(default_spectrum.description, default_spectrum.wavelengths)
Blackbody at 5200.0 K [360.0, 407.0, 454.0, 501.0, 548.0, 595.0, 642.0, 689.0, 736.0, 783.0, 830.0]
Create a volume optical property file#
A *.material file describes how light travels inside a body. It always holds a dispersion model and an absorption curve. Here the dispersion is given by the refractive index at 587.6 nm and the Abbe number, which is how most data sheets report it. Use a MaterialType member to select the material model.
[7]:
pmma = MaterialFile(
description="PMMA",
material_type=MaterialType.isotropic,
dispersion=MaterialConstringence(constringence=57.2, index=1.49),
absorption_wavelengths=[486.0, 532.0, 643.0],
absorption_values=[0.0001, 0.00015, 0.0005],
)
pmma_path = pmma.save(OUTPUT_DIR / "pmma.material")
print(pmma_path.read_text())
OPTIS - Material file v13
PMMA
Isotropic
Constringence
57.2
1.49
3
486 0.0001
532 0.00015
643 0.0005
1
1
0
Nested models stay editable through their validated properties. Switching between an isotropic, metallic, or birefringent flavor requires constructing a new MaterialFile with the corresponding dispersion model.
[8]:
dispersion = pmma.dispersion
assert isinstance(dispersion, MaterialConstringence)
dispersion.index = 1.5
print(dispersion.index)
1.5
Adding a scattering phase function turns the same file into a diffusing material. The diffusion coefficient is given in mm-1 for each wavelength, and the Henyey-Greenstein anisotropy factor drives how forward the scattering is.
[9]:
diffuser = MaterialFile(
description="Diffusing PMMA",
dispersion=MaterialConstringence(constringence=57.2, index=1.49),
absorption_wavelengths=[486.0, 643.0],
absorption_values=[0.0001, 0.0005],
scattering_wavelengths=[480.0, 555.0, 650.0],
scattering_values=[0.016, 0.0165, 0.025],
scattering=VolumeScatteringHenyeyGreenstein(
wavelengths=[480.0, 555.0, 650.0], anisotropies=[0.8, 0.9, 0.9]
),
)
diffuser_path = diffuser.save(OUTPUT_DIR / "diffusing_pmma.material")
print(diffuser_path.read_text())
OPTIS - Material file v13
Diffusing PMMA
Isotropic
Constringence
57.2
1.49
2
486 0.0001
643 0.0005
1
1
1
OPTIS - Volumic Scattering file v1
1
3
480 0.016
555 0.0165
650 0.025
3
480 0.8
555 0.9
650 0.9
A fluorescent material uses the same default index and absorption curves, and starts with an empty dye table. Add a default dye to create the single-row example shown in the Speos material editor.
[10]:
fluorescent = MaterialFile(
material_type=MaterialType.fluorescent,
fluorescent_pigments=[FluorescentPigment(dye=FluorescentPigmentFile())],
)
fluorescent_path = fluorescent.save(OUTPUT_DIR / "fluorescent.material")
print(fluorescent.description, fluorescent_path.read_text())
Optical Glass OPTIS - Material file v13
Optical Glass
Fluorescent
Dispersion_Curve
3
486 1.5
532 1.5
643 1.5
3
486 0
532 0
643 0
1
1
0
1
1
OPTIS - Fluorescent pigment file v3.0
Fluorescent pigment
ISOTROPIC
1
0
OPTIS - Spectrum file v1
Default spectrum : edit or load -->
3
350 0
360 0.1
370 0
OPTIS - Spectrum file v1
Default spectrum : edit or load -->
3
350 95
360 95
370 95
OPTIS - Spectrum file v1
Default spectrum : edit or load -->
3
350 0
360 0.095
370 0
OPTIS - Spectrum file v1
Default spectrum : edit or load -->
3
400 0
500 100
600 0
0
0
Create surface optical property files#
The *.simplescattering format is the quickest way to describe a scattering surface: it does not depend on the wavelength nor on the angle of incidence. This one is a perfect white Lambertian reflector. The Gaussian part is set explicitly to keep the contributions within the 100% budget.
[11]:
white = SimpleScatteringSurfaceFile(
mode=SimpleScatteringMode.reflection, absorption=0.0, lambertian=100.0, gaussian=0.0
)
white_path = white.save(OUTPUT_DIR / "white_diffuser.simplescattering")
print(white_path.read_text())
OPTIS - Simple scattering surface file v2.0
Scattering surface
Reflection
0 100 0 25
The *.scattering format describes the same physics, but every contribution can vary with the angle of incidence and the wavelength. Speos needs at least two wavelengths and expects the angles of incidence to span 0 to 90 degrees.
What is left once the reflection and transmission contributions are summed is absorbed, which the absorption property reports.
[12]:
half_diffusing = ScatteringSurfaceSample(
specular_reflection=30.0,
lambertian_reflection=20.0,
specular_transmission=20.0,
lambertian_transmission=10.0,
)
glazing = ScatteringSurfaceFile(
description="Half diffusing glazing",
wavelengths=[400.0, 700.0],
incident_angles=[0.0, 90.0],
samples=[[half_diffusing, half_diffusing], [half_diffusing, half_diffusing]],
)
glazing_path = glazing.save(OUTPUT_DIR / "glazing.scattering")
print("Absorption at 0 degrees and 400 nm:", glazing.samples[0][0].absorption, "%")
print(glazing_path.read_text())
Absorption at 0 degrees and 400 nm: 20.0 %
OPTIS - Scattering surface file v1.0
Half diffusing glazing
2 2
400 700
0 30 20 30 20
20 10 20 10
0 0 0 0
0 0 0 0
0 0 0 0
90 30 20 30 20
20 10 20 10
0 0 0 0
0 0 0 0
0 0 0 0
The *.coated format describes a non-scattering coating, with the reflection and the transmission given separately for the S and P polarizations. A Speos coating is only valid for the rays crossing the interface in one direction.
[13]:
coating_rows = {
0.0: [(31.9, 68.1), (1.5, 98.5), (25.7, 74.3)],
50.0: [(11.3, 88.7), (11.4, 88.6), (23.3, 76.7)],
90.0: [(100.0, 0.0), (100.0, 0.0), (100.0, 0.0)],
}
coating = CoatedSurfaceFile(
description="Antireflective coating",
wavelengths=[480.0, 580.0, 780.0],
incident_angles=list(coating_rows),
samples=[
[
CoatedSurfaceSample(reflection, transmission, reflection, transmission)
for reflection, transmission in row
]
for row in coating_rows.values()
],
)
coating_path = coating.save(OUTPUT_DIR / "antireflective.coated")
print(coating_path.read_text())
OPTIS - Coated surface file v1.0
Antireflective coating
3 3
480 580 780
0 31.9 68.1 1.5 98.5 25.7 74.3
31.9 68.1 1.5 98.5 25.7 74.3
50 11.3 88.7 11.4 88.6 23.3 76.7
11.3 88.7 11.4 88.6 23.3 76.7
90 100 0 100 0 100 0
100 0 100 0 100 0
Create a ray file#
A ray file holds the rays emitted by a measured or simulated source. Each ray carries a start position in mm, a direction given as cosines, a wavelength in nm and a relative energy. The direction must already be a unit vector at construction or assignment.
[14]:
rays = RayFile(
rays=[
Ray(position=(0.0, 0.0, 0.0), direction=(0.0, 0.0, 1.0), wavelength=633.0),
Ray(position=(0.1, 0.0, 0.0), direction=(0.0, 0.0, 1.0), wavelength=633.0),
Ray(position=(0.0, 0.1, 0.0), direction=(0.0, 0.0, 1.0), wavelength=633.0),
],
radiant_flux=1.0,
luminous_flux=112.0,
)
rays_path = rays.save(OUTPUT_DIR / "collimated.ray")
print(rays_path, rays_path.stat().st_size, "bytes")
/tmp/pyspeos_file_formats_zejo9u9x/collimated.ray 124 bytes
The same rays can be exported to the text flavor that the Speos ray file editor reads. Beware that this flavor does not carry the flux of the file.
[15]:
text_path = rays.save_text(OUTPUT_DIR / "collimated.txt")
print(text_path.read_text())
3
1 0 0 0 0 0 1 633 1
2 0.1 0 0 0 0 1 633 1
3 0 0.1 0 0 0 1 633 1
The Zemax spectral source format carries millimeter coordinates, wavelengths in micrometers, and radiometric ray fluxes. PySpeos converts the units and distributes the total radiant flux using the relative ray energies. SDF does not carry luminous flux, so provide that value explicitly when it is known.
[16]:
sdf_path = rays.save_sdf(OUTPUT_DIR / "collimated.sdf")
sdf_rays = RayFile.load_sdf(sdf_path, luminous_flux=rays.luminous_flux)
print(sdf_path, len(sdf_rays.rays), "rays", sdf_rays.radiant_flux, "W")
print([ray.energy for ray in sdf_rays.rays])
/tmp/pyspeos_file_formats_zejo9u9x/collimated.sdf 3 rays 1.0 W
[0.3333333333333333, 0.3333333333333333, 0.3333333333333333]
Create a 3D texture mapping file#
A Speos 3D Texture is made of a mesh and of a *.OPT3DMapping file, which lays out where each pattern sits on the support. That mapping file is the recipe handed over to manufacture the physical part, so it is often generated or post-processed outside of Speos.
Below, a rectangular grid of patterns is laid out, with a scale growing along the grid so that the light extraction increases away from the source.
[17]:
patterns = []
for row in range(5):
for column in range(10):
scale = round(0.5 + 0.1 * column, 2)
patterns.append(
TexturePattern(
position=(column * 2.0, row * 2.0, 0.0),
scale=(scale, scale, scale),
)
)
mapping_path = Texture3DMappingFile(patterns=patterns).save(OUTPUT_DIR / "light_guide.OPT3DMapping")
print("\n".join(mapping_path.read_text().splitlines()[:4]))
50
0 0 0 1 0 0 0 1 0 0.5 0.5 0.5
2 0 0 1 0 0 0 1 0 0.6 0.6 0.6
4 0 0 1 0 0 0 1 0 0.7 0.7 0.7
Reading the mapping back gives every pattern placement, ready to be inspected or converted to the format expected by a tooling supplier.
[18]:
mapping = Texture3DMappingFile.load(mapping_path)
print(len(mapping.patterns), "patterns")
print(mapping.patterns[-1])
50 patterns
<ansys.speos.core.input_files.texture_3d.TexturePattern object at 0x7f4ca0df6c30>
Download this example
Download this example as a Jupyter Notebook or as a Python script. All assets used in the examples can be downloaded as a ZIP archive.