From 8b9c74142e1820452216cc6c6afd33a6343187c7 Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Mon, 13 Oct 2025 11:30:07 +0200 Subject: [PATCH 01/23] Update dependencies: upgrade Cython to 3.1 and numpy to >=2; adjust raysect version to 0.9.1.* --- pyproject.toml | 2 +- requirements.txt | 6 +++--- setup.py | 17 +++++++++-------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4849f0b5..e198bb5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [build-system] -requires = ["setuptools>=62.3", "oldest-supported-numpy", "cython~=3.0", "raysect==0.8.1.*"] +requires = ["setuptools>=62.3", "numpy", "cython~=3.1", "raysect==0.9.1.*"] build-backend="setuptools.build_meta" diff --git a/requirements.txt b/requirements.txt index 9a13464d..7fea14d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -cython~=3.0 -numpy>=1.14,<2.0 +cython~=3.1 +numpy>=2 scipy matplotlib -raysect==0.8.1.* +raysect==0.9.1.* diff --git a/setup.py b/setup.py index f11dd08f..8699c4f5 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,15 @@ -from collections import defaultdict -import sys +import multiprocessing import os import os.path as path +import sys +from collections import defaultdict from pathlib import Path -import multiprocessing + import numpy -from setuptools import setup, find_packages, Extension from Cython.Build import cythonize +from setuptools import Extension, find_packages, setup -multiprocessing.set_start_method('fork') +multiprocessing.set_start_method("fork") force = False profile = False @@ -117,14 +118,14 @@ long_description=long_description, long_description_content_type="text/markdown", install_requires=[ - "numpy>=1.14,<2.0", + "numpy>=2", "scipy", "matplotlib", - "raysect==0.8.1.*", + "raysect==0.9.1.*", ], extras_require={ # Running ./dev/build_docs.sh runs setup.py, which requires cython. - "docs": ["cython~=3.0", "sphinx", "sphinx-rtd-theme", "sphinx-tabs"], + "docs": ["cython~=3.1", "sphinx", "sphinx-rtd-theme", "sphinx-tabs"], }, packages=find_packages(include=["cherab*"]), package_data={"": [ From d059ed82c30408a9f2b1f6104fd07b2677fa6b68 Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Mon, 13 Oct 2025 11:37:43 +0200 Subject: [PATCH 02/23] Update CI configuration: switch to ubuntu-latest, adjust Python and numpy versions, and upgrade Raysect to 0.9.* --- .github/workflows/ci.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da0d082c..73fcdbd9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,12 +7,11 @@ on: jobs: tests: name: Run tests - runs-on: ubuntu-22.04 # Needed for Python 3.7 compatibility + runs-on: ubuntu-latest strategy: fail-fast: false matrix: - numpy-version: ["oldest-supported-numpy", "'numpy<2'"] - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Checkout code uses: actions/checkout@v2 @@ -23,9 +22,9 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install Python dependencies - run: python -m pip install --prefer-binary cython~=3.0 ${{ matrix.numpy-version }} scipy matplotlib "pyopencl[pocl]>=2022.2.4" + run: python -m pip install --prefer-binary cython~=3.1 numpy>=2 scipy matplotlib "pyopencl[pocl]>=2022.2.4" - name: Install Raysect from pypi - run: pip install raysect==0.8.1.* + run: pip install raysect==0.9.* - name: Build cherab run: dev/build.sh - name: Run tests From 3ec32a1b34f4e1798c561b6e94cb5294a12f4c42 Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Mon, 13 Oct 2025 11:38:26 +0200 Subject: [PATCH 03/23] Refactor: Correct spelling of 'Targetted' to 'Targeted' in bolometry and targettedpixel modules --- cherab/tools/observers/bolometry.py | 16 +++++------ .../tools/observers/group/targettedpixel.py | 28 +++++++++---------- cherab/tools/tests/test_observer_groups.py | 16 +++++------ 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/cherab/tools/observers/bolometry.py b/cherab/tools/observers/bolometry.py index 20179796..5cd8c1a2 100644 --- a/cherab/tools/observers/bolometry.py +++ b/cherab/tools/observers/bolometry.py @@ -22,12 +22,12 @@ import numpy as np from raysect.core import Node, translate, rotate_basis, Point3D, Vector3D, Ray as CoreRay, Primitive, World -from raysect.core.math.sampler import TargettedHemisphereSampler, RectangleSampler3D +from raysect.core.math.sampler import TargetedHemisphereSampler, RectangleSampler3D from raysect.primitive import Box, Cylinder, Subtract, Union from raysect.optical.observer import PowerPipeline0D, RadiancePipeline0D, \ - SpectralPowerPipeline0D, SpectralRadiancePipeline0D, SightLine, TargettedPixel + SpectralPowerPipeline0D, SpectralRadiancePipeline0D, SightLine, TargetedPixel from raysect.optical.observer import PowerPipeline2D, RadiancePipeline2D, \ - SpectralPowerPipeline2D, SpectralRadiancePipeline2D, TargettedCCDArray + SpectralPowerPipeline2D, SpectralRadiancePipeline2D, TargetedCCDArray from raysect.optical.material.material import NullMaterial from raysect.optical.material import AbsorbingSurface @@ -351,7 +351,7 @@ def curvature_radius(self): return self._curvature_radius -class BolometerFoil(TargettedPixel): +class BolometerFoil(TargetedPixel): """ A rectangular foil bolometer detector. @@ -447,7 +447,7 @@ def __init__(self, detector_id, centre_point, basis_x, dx, basis_y, dy, slit, translation = translate(centre_point.x, centre_point.y, centre_point.z) rotation = rotate_basis(normal_vec, basis_y) - super().__init__([slit.target], targetted_path_prob=1.0, + super().__init__([slit.target], targeted_path_prob=1.0, pixel_samples=1000, x_width=dx, y_width=dy, spectral_bins=1, quiet=True, parent=parent, transform=translation * rotation, name=detector_id) @@ -662,7 +662,7 @@ def calculate_etendue(self, ray_count=10000, batches=10, max_distance=1e999): sphere = target.bounding_sphere() spheres = [(sphere.centre.transform(self.to_local()), sphere.radius, 1.0)] # instance targetted pixel sampler to sample directions - targetted_sampler = TargettedHemisphereSampler(spheres) + targetted_sampler = TargetedHemisphereSampler(spheres) # instance rectangle pixel sampler to sample origins point_sampler = RectangleSampler3D(width=self.x_width, height=self.y_width) @@ -701,7 +701,7 @@ def etendue_single_run(_): return etendue, etendue_error -class BolometerIRVB(TargettedCCDArray): +class BolometerIRVB(TargetedCCDArray): """ A rectangular infra red video bolometer (IRVB). @@ -784,7 +784,7 @@ def __init__(self, name, width, pixels, slit, transform, parent=None, self._accumulate = None # Will be set after pipeline is created. super().__init__([slit.target], pixels=pixels, width=width, - targetted_path_prob=0.99, parent=parent, pipelines=[], + targeted_path_prob=0.99, parent=parent, pipelines=[], transform=transform, name=name) self.pixel_samples = 1000 self.spectral_bins = 1 diff --git a/cherab/tools/observers/group/targettedpixel.py b/cherab/tools/observers/group/targettedpixel.py index 07d13e7f..e440f70e 100644 --- a/cherab/tools/observers/group/targettedpixel.py +++ b/cherab/tools/observers/group/targettedpixel.py @@ -17,14 +17,14 @@ # under the Licence. from numpy import ndarray -from raysect.optical.observer import TargettedPixel +from raysect.optical.observer import TargetedPixel from .base import Observer0DGroup class TargettedPixelGroup(Observer0DGroup): """ - A group of targetted pixel under a single scene-graph node. + A group of targeted pixel under a single scene-graph node. A scene-graph object regrouping a series of 'TargettedPixel' observers as a scene-graph parent. Allows combined observation and display @@ -33,9 +33,10 @@ class TargettedPixelGroup(Observer0DGroup): :ivar list x_width: Width of pixel along local x axis :ivar list y_width: Width of pixel along local y axis :ivar list targets: Targets for preferential sampling - :ivar list targetted_path_prob: Probability of ray being casted at the target + :ivar list targeted_path_prob: Probability of ray being casted at the target """ - _OBSERVER_TYPE = TargettedPixel + + _OBSERVER_TYPE = TargetedPixel @property def x_width(self): @@ -76,7 +77,7 @@ def targets(self): """ List of target lists used by pixels for preferential sampling - :param list value: List of primitives to be set to each pixel or + :param list value: List of primitives to be set to each pixel or list of lists containing targets specific for each pixel in this case the number of lists must match number of pixels @@ -99,18 +100,17 @@ def targets(self, value): pixel.targets = value @property - def targetted_path_prob(self): - return [pixel.targetted_path_prob for pixel in self._observers] - - @targetted_path_prob.setter - def targetted_path_prob(self, value): + def targeted_path_prob(self): + return [pixel.targeted_path_prob for pixel in self._observers] + + @targeted_path_prob.setter + def targeted_path_prob(self, value): if isinstance(value, (list, tuple)): if len(value) == len(self._observers): for pixel, v in zip(self._observers, value): - pixel.targetted_path_prob = v + pixel.targeted_path_prob = v else: - raise ValueError("The length of 'value' ({}) " - "mismatches the number of pixels ({}).".format(len(value), len(self._observers))) + raise ValueError("The length of 'value' ({}) mismatches the number of pixels ({}).".format(len(value), len(self._observers))) else: for pixel in self._observers: - pixel.targetted_path_prob = value + pixel.targeted_path_prob = value diff --git a/cherab/tools/tests/test_observer_groups.py b/cherab/tools/tests/test_observer_groups.py index 1f5b7cb0..a04f095c 100644 --- a/cherab/tools/tests/test_observer_groups.py +++ b/cherab/tools/tests/test_observer_groups.py @@ -1,7 +1,7 @@ import unittest from raysect.core.workflow import RenderEngine -from raysect.optical.observer import Observer0D, SightLine, FibreOptic, Pixel, TargettedPixel, PowerPipeline0D, SpectralPowerPipeline0D +from raysect.optical.observer import Observer0D, SightLine, FibreOptic, Pixel, TargetedPixel, PowerPipeline0D, SpectralPowerPipeline0D from raysect.primitive import Sphere from cherab.tools.observers.group.base import Observer0DGroup @@ -352,7 +352,7 @@ class TargettedPixelGroupTestCase(PixelGroupTestCase): _GROUP_CLASS = TargettedPixelGroup def setUp(self): - self.observers = [TargettedPixel(targets=[Sphere()], pipelines=[PowerPipeline0D()]) for _ in range(self._NUM)] + self.observers = [TargetedPixel(targets=[Sphere()], pipelines=[PowerPipeline0D()]) for _ in range(self._NUM)] def test_targets(self): group = self._GROUP_CLASS(observers=self.observers) @@ -376,13 +376,13 @@ def test_targets(self): # targetted path prob prob = [0.9, 0.95, 1] - group.targetted_path_prob = prob - self.assertListEqual(group.targetted_path_prob, prob) + group.targeted_path_prob = prob + self.assertListEqual(group.targeted_path_prob, prob) prob = 0.8 - group.targetted_path_prob = prob - for group_targetted_path_prob in group.targetted_path_prob: - self.assertEqual(group_targetted_path_prob, prob) + group.targeted_path_prob = prob + for group_targeted_path_prob in group.targeted_path_prob: + self.assertEqual(group_targeted_path_prob, prob) with self.assertRaises(ValueError): - group.targetted_path_prob = [0.7] * (len(group) + 1) + group.targeted_path_prob = [0.7] * (len(group) + 1) From 10d690feb7abc365acbe4383a17bd1699957f14c Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Mon, 13 Oct 2025 11:44:29 +0200 Subject: [PATCH 04/23] Fix remaining typo `targetted` to `targeted` --- cherab/core/model/laser/profile.pyx | 60 +++--- cherab/tools/observers/__init__.py | 14 +- cherab/tools/observers/bolometry.py | 201 +++++++++--------- cherab/tools/observers/group/__init__.py | 2 +- .../tools/observers/group/targettedpixel.py | 25 ++- cherab/tools/tests/test_observer_groups.py | 70 +++--- docs/source/tools/observers.rst | 14 +- 7 files changed, 200 insertions(+), 186 deletions(-) diff --git a/cherab/core/model/laser/profile.pyx b/cherab/core/model/laser/profile.pyx index 82376980..80b73d02 100644 --- a/cherab/core/model/laser/profile.pyx +++ b/cherab/core/model/laser/profile.pyx @@ -4,7 +4,7 @@ from raysect.primitive import Cylinder from raysect.optical cimport Spectrum, Vector3D, translate from cherab.core.laser cimport Laser, LaserProfile -from cherab.core.model.laser.math_functions cimport ConstantAxisymmetricGaussian3D, ConstantBivariateGaussian3D, TrivariateGaussian3D, GaussianBeamModel +from cherab.core.model.laser.math_functions cimport ConstantAxisymmetricGaussian3D, ConstantBivariateGaussian3D, TrivariateGaussian3D, GaussianBeamModel from cherab.core.utility.constants cimport SPEED_OF_LIGHT @@ -22,20 +22,20 @@ cdef class UniformEnergyDensity(LaserProfile): The methods get_pointing, get_polarization and get_energy_density are not limited to the inside of the laser cylinder. If called alone for position (x, y, z) outisde the laser cylinder, they will still return non-zero values. - + In the following example, a laser of length of 2 m (extending from z=0 to z=2 m) with a radius of 3 cm and volumetric energy density of 5 J*m^-3 and polarisation in the y direction is created: .. code-block:: pycon - + >>> from raysect.core import Vector3D >>> from cherab.core.model.laser import UniformEnergyDensity - + >>> energy = 5 # energy density in J >>> radius = 3e-2 # laser radius in m >>> length = 2 # laser length in m >>> polarisation = Vector3D(0, 1, 0) # polarisation direction - + # create the laser profile >>> laser_profile = UniformEnergyDensity(energy, radius, length, polarisation) @@ -108,7 +108,7 @@ cdef class UniformEnergyDensity(LaserProfile): cpdef list generate_geometry(self): return generate_segmented_cylinder(self.laser_radius, self.laser_length) - + cdef class ConstantBivariateGaussian(LaserProfile): """ @@ -120,8 +120,8 @@ cdef class ConstantBivariateGaussian(LaserProfile): The model imitates a laser beam with a uniform power output within a single pulse. This results in the distribution of the energy density along the propagation direction of the laser (z-axis) to be also uniform. The integral value of laser energy Exy in an x-y plane is given by - - .. math:: + + .. math:: E_{xy} = \\frac{E_p}{(c * \\tau)}, where Ep is the energy of the laser pulse, tau is the temporal pulse length and c is the speed of light in vacuum. @@ -133,23 +133,23 @@ cdef class ConstantBivariateGaussian(LaserProfile): The sigma_x and sigma_y are standard deviations in x and y directions, respectively. .. note:: - The height of the cylinder, forming the laser beam, is given by the laser_length and is independent from the + The height of the cylinder, forming the laser beam, is given by the laser_length and is independent from the temporal length of the laser pulse given by pulse_length. This gives the possibility to independently control the size of the laser primitive and the value of the volumetric energy density. - + The methods get_pointing, get_polarization and get_energy_density are not limited to the inside of the laser cylinder. If called for position (x, y, z) outisde the laser cylinder, they can still return non-zero values. - + The following example shows how to create a laser with sigma_x= 1 cm and sigma_y=2 cm, which makes the laser profile in x-y plane to be elliptical. The pulse energy is 5 J and the laser temporal pulse length is 10 ns: .. code-block:: pycon - + >>> from raysect.core import Vector3D >>> from cherab.core.model.laser import ConstantBivariateGaussian - + >>> radius = 3e-2 # laser radius in m >>> length = 2 # laser length in m >>> polarisation = Vector3D(0, 1, 0) # polarisation direction @@ -157,7 +157,7 @@ cdef class ConstantBivariateGaussian(LaserProfile): >>> pulse_length = 1e-8 # pulse length in s >>> width_x = 1e-2 # standard deviation in x direction in m >>> width_y = 2e-2 # standard deviation in y direction in m - + # create the laser profile >>> laser_profile = ConstantBivariateGaussian(pulse_energy, pulse_length, radius, length, width_x, width_y, polarisation) @@ -323,7 +323,7 @@ cdef class TrivariateGaussian(LaserProfile): The sigma_x and sigma_y are standard deviations in x and y directions, respectively, and E_p is the energy deliverd by laser in a single laser pulse. The mu_z is the mean of the distribution in the z direction and controls th position of the laser pulse along the z direction. - The standard deviation in z direction sigma_z is calculated from the pulse length tau_p, which is the + The standard deviation in z direction sigma_z is calculated from the pulse length tau_p, which is the standard deviation of the Gaussian distributed ouput power of the laser within a single pulse: .. math:: @@ -332,24 +332,24 @@ cdef class TrivariateGaussian(LaserProfile): The c stands for the speed of light in vacuum. .. note:: - The height of the cylinder, forming the laser beam, is given by the laser_length and is independent from the + The height of the cylinder, forming the laser beam, is given by the laser_length and is independent from the temporal length of the laser pulse given by pulse_length. This gives the possibility to independently control the size of the laser primitive and the value of the volumetric energy density. - + The methods get_pointing, get_polarization and get_energy_density are not limited to the inside of the laser cylinder. If called alone for position (x, y, z) outisde the laser cylinder, they can still return non-zero values. - + The following example shows how to create a laser with sigma_x = 1 cm and sigma_y = 2 cm, which makes the laser profile in an x-y plane to be elliptical. The pulse energy is 5 J and the laser temporal pulse length is 10 ns. The position of the laser pulse maximum mean_z is set to 0.5: .. code-block:: pycon - + >>> from raysect.core import Vector3D >>> from cherab.core.model.laser import ConstantBivariateGaussian - + >>> radius = 3e-2 # laser radius in m >>> length = 2 # laser length in m >>> polarisation = Vector3D(0, 1, 0) # polarisation direction @@ -358,7 +358,7 @@ cdef class TrivariateGaussian(LaserProfile): >>> pulse_z = 0.5 # position of the pulse mean >>> width_x = 1e-2 # standard deviation in x direction in m >>> width_y = 2e-2 # standard deviation in y direction in m - + # create the laser profile >>> laser_profile = ConstantBivariateGaussian(pulse_energy, pulse_length, pulse_z, radius, length, width_x, width_y, polarisation) @@ -512,7 +512,7 @@ cdef class TrivariateGaussian(LaserProfile): self._distribution = TrivariateGaussian3D(self._mean_z, self._stddev_x, self._stddev_y, self._stddev_z) - normalisation = self._pulse_energy + normalisation = self._pulse_energy function = normalisation * self._distribution self.set_energy_density_function(function) @@ -541,16 +541,16 @@ cdef class GaussianBeamAxisymmetric(LaserProfile): .. math:: z_R = \\frac{\\pi \\omega_0^2 n}{\\lambda_l} - + where the omega_0 is the standard deviation in the xy plane in the focal point (beam waist) and lambda_l is the central wavelength of the laser. The E_xy stand for the laser energy in an xy plane and is calculated as: - + .. math:: E_{xy} = \\frac{E_p}{(c * \\tau)}, where the E_p is the energy in a single laser pulse and tau is the temporal pulse length. - .. note:: + .. note:: For more information about the Gaussian beam model see https://en.wikipedia.org/wiki/Gaussian_beam The methods get_pointing, get_polarization and get_energy_density are not limited to the inside @@ -562,10 +562,10 @@ cdef class GaussianBeamAxisymmetric(LaserProfile): waist is z=50 cm. The laser wavelength is 1060 nm. .. code-block:: pycon - + >>> from raysect.core import Vector3D >>> from cherab.core.model.laser import GaussianBeamAxisymmetric - + >>> radius = 5e-2 # laser radius in m >>> length = 2 # laser length in m >>> polarisation = Vector3D(0, 1, 0) # polarisation direction @@ -576,7 +576,7 @@ cdef class GaussianBeamAxisymmetric(LaserProfile): >>> width_x = 1e-2 # standard deviation in x direction in m >>> width_y = 2e-2 # standard deviation in y direction in m >>> laser_wlen = 1060 # laser wavelength in nm - + # create the laser profile >>> laser_profile = GaussianBeamAxisymmetric(pulse_energy, pulse_length, length, radius, waist_z, waist_width, laser_wlen) @@ -738,7 +738,7 @@ def generate_segmented_cylinder(radius, length): Generates a segmented cylindrical laser geometry Approximates a long cylinder with a cylindrical segments to optimize - targetted and importance sampling. The height of a cylinder segments is roughly + targeted and importance sampling. The height of a cylinder segments is roughly 2 * cylinder radius. :return: List of cylinders @@ -761,5 +761,5 @@ def generate_segmented_cylinder(radius, length): geometry.append(segment) else: raise ValueError("Incorrect number of segments calculated.") - + return geometry \ No newline at end of file diff --git a/cherab/tools/observers/__init__.py b/cherab/tools/observers/__init__.py index d134ef63..670dfce5 100644 --- a/cherab/tools/observers/__init__.py +++ b/cherab/tools/observers/__init__.py @@ -1,4 +1,3 @@ - # Copyright 2016-2018 Euratom # Copyright 2016-2018 United Kingdom Atomic Energy Authority # Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas @@ -17,8 +16,15 @@ # See the Licence for the specific language governing permissions and limitations # under the Licence. -from .bolometry import BolometerCamera, BolometerFoil, BolometerSlit, BolometerIRVB +from .bolometry import BolometerCamera, BolometerFoil, BolometerIRVB, BolometerSlit from .calcam import load_calcam_calibration +from .group import ( + FibreOpticGroup, + PixelGroup, + SightLineGroup, + SpectroscopicFibreOpticGroup, + SpectroscopicSightLineGroup, + TargetedPixelGroup, +) from .intersections import find_wall_intersection -from .spectroscopy import SpectroscopicSightLine, SpectroscopicFibreOptic -from .group import PixelGroup, TargettedPixelGroup, SightLineGroup, FibreOpticGroup, SpectroscopicFibreOpticGroup, SpectroscopicSightLineGroup +from .spectroscopy import SpectroscopicFibreOptic, SpectroscopicSightLine diff --git a/cherab/tools/observers/bolometry.py b/cherab/tools/observers/bolometry.py index 5cd8c1a2..532d2826 100644 --- a/cherab/tools/observers/bolometry.py +++ b/cherab/tools/observers/bolometry.py @@ -1,4 +1,3 @@ - # Copyright 2016-2018 Euratom # Copyright 2016-2018 United Kingdom Atomic Energy Authority # Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas @@ -17,23 +16,32 @@ # See the Licence for the specific language governing permissions and limitations # under the Licence. -from enum import Enum import functools -import numpy as np +from enum import Enum -from raysect.core import Node, translate, rotate_basis, Point3D, Vector3D, Ray as CoreRay, Primitive, World -from raysect.core.math.sampler import TargetedHemisphereSampler, RectangleSampler3D -from raysect.primitive import Box, Cylinder, Subtract, Union -from raysect.optical.observer import PowerPipeline0D, RadiancePipeline0D, \ - SpectralPowerPipeline0D, SpectralRadiancePipeline0D, SightLine, TargetedPixel -from raysect.optical.observer import PowerPipeline2D, RadiancePipeline2D, \ - SpectralPowerPipeline2D, SpectralRadiancePipeline2D, TargetedCCDArray -from raysect.optical.material.material import NullMaterial +import numpy as np +from raysect.core import Node, Point3D, Primitive, Vector3D, World, rotate_basis, translate +from raysect.core import Ray as CoreRay +from raysect.core.math.sampler import RectangleSampler3D, TargetedHemisphereSampler from raysect.optical.material import AbsorbingSurface +from raysect.optical.material.material import NullMaterial +from raysect.optical.observer import ( + PowerPipeline0D, + PowerPipeline2D, + RadiancePipeline0D, + RadiancePipeline2D, + SightLine, + SpectralPowerPipeline0D, + SpectralPowerPipeline2D, + SpectralRadiancePipeline0D, + SpectralRadiancePipeline2D, + TargetedCCDArray, + TargetedPixel, +) +from raysect.primitive import Box, Cylinder, Subtract, Union from cherab.tools.inversions.voxels import VoxelCollection - R_2_PI = 1 / (2 * np.pi) @@ -71,8 +79,7 @@ class BolometerCamera(Node): >>> camera = BolometerCamera(name="MyBolometer", parent=world) """ - def __init__(self, camera_geometry=None, parent=None, transform=None, name=''): - + def __init__(self, camera_geometry=None, parent=None, transform=None, name=""): super().__init__(parent=parent, transform=transform, name=name) self._foil_detectors = [] @@ -133,12 +140,8 @@ def foil_detectors(self): @foil_detectors.setter def foil_detectors(self, value): - if not isinstance(value, list): - raise TypeError( - "The foil_detectors attribute of BolometerCamera must be a list of " - "BolometerFoils or BolometerIRVBs." - ) + raise TypeError("The foil_detectors attribute of BolometerCamera must be a list of BolometerFoils or BolometerIRVBs.") # Prevent external changes being made to this list value = value.copy() @@ -148,8 +151,8 @@ def foil_detectors(self, value): "The foil_detectors attribute of BolometerCamera must be a list of " "BolometerFoil or BolometerIRVB objects. Value {} is not a BolometerFoil " "or BolometerIRVB.".format(foil_detector) - ) - if not foil_detector.slit in self._slits: + ) + if foil_detector.slit not in self._slits: self._slits.append(foil_detector.slit) foil_detector.parent = self @@ -167,11 +170,9 @@ def add_foil_detector(self, foil_detector): """ if not isinstance(foil_detector, (BolometerFoil, BolometerIRVB)): - raise TypeError( - "The foil_detector argument must be of type BolometerFoil or BolometerIRVB." - ) + raise TypeError("The foil_detector argument must be of type BolometerFoil or BolometerIRVB.") - if not foil_detector.slit in self._slits: + if foil_detector.slit not in self._slits: self._slits.append(foil_detector.slit) foil_detector.parent = self @@ -213,7 +214,7 @@ class BolometerSlit(Node): larger than the slit dx and dy, which can cause partial occlusion of nearby primitives. It also relies on no rays being launched with directions outside the solid angle of the aperture's bounding sphere: depending on the - foil-slit distance and slit size, and also the foil's targetted_path_prob, + foil-slit distance and slit size, and also the foil's targeted_path_prob, this may not be guaranteed. Supplying a proper mesh geometry for the camera is recommended instead of using a CSG aperture. @@ -255,9 +256,7 @@ class BolometerSlit(Node): >>> slit = BolometerSlit("slit", centre_point, basis_x, dx, basis_y, dy, parent=camera) """ - def __init__(self, slit_id, centre_point, basis_x, dx, basis_y, dy, dz=0.001, - parent=None, csg_aperture=False, curvature_radius=0): - + def __init__(self, slit_id, centre_point, basis_x, dx, basis_y, dy, dz=0.001, parent=None, csg_aperture=False, curvature_radius=0): # perform validation of input parameters if not isinstance(dx, (float, int)): @@ -274,11 +273,9 @@ def __init__(self, slit_id, centre_point, basis_x, dx, basis_y, dy, dz=0.001, raise TypeError("centre_point argument for BolometerSlit must be of type Point3D.") if not isinstance(curvature_radius, (float, int)): - raise TypeError("curvature_radius argument for BolometerSlit " - "must be of type float/int.") + raise TypeError("curvature_radius argument for BolometerSlit must be of type float/int.") if curvature_radius < 0: - raise ValueError("curvature_radius argument for BolometerSlit " - "must not be negative.") + raise ValueError("curvature_radius argument for BolometerSlit must not be negative.") if not isinstance(basis_x, Vector3D): raise TypeError("The basis vectors of BolometerSlit must be of type Vector3D.") @@ -300,8 +297,14 @@ def __init__(self, slit_id, centre_point, basis_x, dx, basis_y, dy, dz=0.001, super().__init__(parent=parent, transform=transform, name=slit_id) - self.target = Box(lower=Point3D(-dx/2*1.01, -dy/2*1.01, -dz/2), upper=Point3D(dx/2*1.01, dy/2*1.01, dz/2), - transform=None, material=NullMaterial(), parent=self, name=slit_id+' - target') + self.target = Box( + lower=Point3D(-dx / 2 * 1.01, -dy / 2 * 1.01, -dz / 2), + upper=Point3D(dx / 2 * 1.01, dy / 2 * 1.01, dz / 2), + transform=None, + material=NullMaterial(), + parent=self, + name=slit_id + " - target", + ) self._csg_aperture = None self.csg_aperture = csg_aperture @@ -332,14 +335,14 @@ def csg_aperture(self): @csg_aperture.setter def csg_aperture(self, value): - if value is True: width = max(self.dx, self.dy) - face = Box(Point3D(-width, -width, -self.dz/2), Point3D(width, width, self.dz/2)) - slit = Box(lower=Point3D(-self.dx/2, -self.dy/2, -self.dz/2 - self.dz*0.1), - upper=Point3D(self.dx/2, self.dy/2, self.dz/2 + self.dz*0.1)) - self._csg_aperture = Subtract(face, slit, parent=self, - material=AbsorbingSurface(), name=self.name+' - CSG Aperture') + face = Box(Point3D(-width, -width, -self.dz / 2), Point3D(width, width, self.dz / 2)) + slit = Box( + lower=Point3D(-self.dx / 2, -self.dy / 2, -self.dz / 2 - self.dz * 0.1), + upper=Point3D(self.dx / 2, self.dy / 2, self.dz / 2 + self.dz * 0.1), + ) + self._csg_aperture = Subtract(face, slit, parent=self, material=AbsorbingSurface(), name=self.name + " - CSG Aperture") else: if isinstance(self._csg_aperture, Primitive): @@ -403,9 +406,9 @@ class BolometerFoil(TargetedPixel): >>> detector = BolometerFoil("ch#1", centre_point, basis_x, dx, basis_y, dy, slit, parent=camera) """ - def __init__(self, detector_id, centre_point, basis_x, dx, basis_y, dy, slit, - parent=None, units="Power", accumulate=False, curvature_radius=0): - + def __init__( + self, detector_id, centre_point, basis_x, dx, basis_y, dy, slit, parent=None, units="Power", accumulate=False, curvature_radius=0 + ): # perform validation of input parameters if not isinstance(dx, (float, int)): @@ -425,11 +428,9 @@ def __init__(self, detector_id, centre_point, basis_x, dx, basis_y, dy, slit, raise TypeError("centre_point argument for BolometerFoil must be of type Point3D.") if not isinstance(curvature_radius, (float, int)): - raise TypeError("curvature_radius argument for BolometerFoil " - "must be of type float/int.") + raise TypeError("curvature_radius argument for BolometerFoil must be of type float/int.") if curvature_radius < 0: - raise ValueError("curvature_radius argument for BolometerFoil " - "must not be negative.") + raise ValueError("curvature_radius argument for BolometerFoil must not be negative.") if not isinstance(basis_x, Vector3D): raise TypeError("The basis vectors of BolometerFoil must be of type Vector3D.") @@ -447,9 +448,18 @@ def __init__(self, detector_id, centre_point, basis_x, dx, basis_y, dy, slit, translation = translate(centre_point.x, centre_point.y, centre_point.z) rotation = rotate_basis(normal_vec, basis_y) - super().__init__([slit.target], targeted_path_prob=1.0, - pixel_samples=1000, x_width=dx, y_width=dy, spectral_bins=1, quiet=True, - parent=parent, transform=translation * rotation, name=detector_id) + super().__init__( + [slit.target], + targeted_path_prob=1.0, + pixel_samples=1000, + x_width=dx, + y_width=dy, + spectral_bins=1, + quiet=True, + parent=parent, + transform=translation * rotation, + name=detector_id, + ) # Update pipeline based on units self.units = units @@ -530,16 +540,13 @@ def as_sightline(self): else: raise ValueError("The units argument of BolometerFoil must be one of 'Power' or 'Radiance'.") - los_observer = SightLine(pipelines=[pipeline], pixel_samples=1, quiet=True, - parent=self, name=self.name) + los_observer = SightLine(pipelines=[pipeline], pixel_samples=1, quiet=True, parent=self, name=self.name) los_observer.render_engine = self.render_engine los_observer.spectral_bins = self.spectral_bins los_observer.min_wavelength = self.min_wavelength los_observer.max_wavelength = self.max_wavelength # The observer's Z axis should be aligned along the line of sight vector - los_observer.transform = rotate_basis( - self.sightline_vector.transform(self.to_local()), self.basis_y - ) + los_observer.transform = rotate_basis(self.sightline_vector.transform(self.to_local()), self.basis_y) return los_observer @@ -560,7 +567,6 @@ def trace_sightline(self): direction = self.sightline_vector while True: - # Find the next intersection point of the ray with the world intersection = self.root.hit(CoreRay(origin, direction)) @@ -661,8 +667,8 @@ def calculate_etendue(self, ray_count=10000, batches=10, max_distance=1e999): # generate bounding sphere and convert to local coordinate system sphere = target.bounding_sphere() spheres = [(sphere.centre.transform(self.to_local()), sphere.radius, 1.0)] - # instance targetted pixel sampler to sample directions - targetted_sampler = TargetedHemisphereSampler(spheres) + # instance targeted pixel sampler to sample directions + targeted_sampler = TargetedHemisphereSampler(spheres) # instance rectangle pixel sampler to sample origins point_sampler = RectangleSampler3D(width=self.x_width, height=self.y_width) @@ -671,8 +677,8 @@ def etendue_single_run(_): origins = point_sampler(samples=ray_count) passed = 0.0 for origin in origins: - # obtain targetted vector sample - direction, pdf = targetted_sampler(origin, pdf=True) + # obtain targeted vector sample + direction, pdf = targeted_sampler(origin, pdf=True) path_weight = R_2_PI * direction.z / pdf # Transform to world space origin = origin.transform(detector_transform) @@ -758,14 +764,10 @@ class BolometerIRVB(TargetedCCDArray): >>> detector = BolometerIRVB("irvb", width, pixels, slit, transform, parent=camera) """ - _PIPELINES = {_Units.POWER: PowerPipeline2D, - _Units.RADIANCE: RadiancePipeline2D} - _SPECTRAL_PIPELINES = {_Units.POWER: SpectralPowerPipeline2D, - _Units.RADIANCE: SpectralRadiancePipeline2D} - - def __init__(self, name, width, pixels, slit, transform, parent=None, - units="power", accumulate=False, curvature_radius=0): + _PIPELINES = {_Units.POWER: PowerPipeline2D, _Units.RADIANCE: RadiancePipeline2D} + _SPECTRAL_PIPELINES = {_Units.POWER: SpectralPowerPipeline2D, _Units.RADIANCE: SpectralRadiancePipeline2D} + def __init__(self, name, width, pixels, slit, transform, parent=None, units="power", accumulate=False, curvature_radius=0): # perform validation of input parameters width = float(width) if width < 0: @@ -776,16 +778,15 @@ def __init__(self, name, width, pixels, slit, transform, parent=None, curvature_radius = float(curvature_radius) if curvature_radius < 0: - raise ValueError("curvature_radius argument for BolometerIRVB " - "must not be negative.") + raise ValueError("curvature_radius argument for BolometerIRVB must not be negative.") self._slit = slit self._curvature_radius = curvature_radius self._accumulate = None # Will be set after pipeline is created. - super().__init__([slit.target], pixels=pixels, width=width, - targeted_path_prob=0.99, parent=parent, pipelines=[], - transform=transform, name=name) + super().__init__( + [slit.target], pixels=pixels, width=width, targeted_path_prob=0.99, parent=parent, pipelines=[], transform=transform, name=name + ) self.pixel_samples = 1000 self.spectral_bins = 1 self.quiet = True @@ -815,18 +816,22 @@ def pixels_as_foils(self): for x in range(nx): pixel_column = [] for y in range(ny): - pixel_centre = (foil_bottom_left - + (x + 0.5) * XAXIS * pixel_width - + (y + 0.5) * YAXIS * pixel_height) + pixel_centre = foil_bottom_left + (x + 0.5) * XAXIS * pixel_width + (y + 0.5) * YAXIS * pixel_height pixel = BolometerFoil( detector_id="IRVB pixel ({},{})".format(x + 1, y + 1), - centre_point=pixel_centre, basis_x=XAXIS, dx=pixel_width, - basis_y=YAXIS, dy=pixel_height, slit=self._slit, - units=self._units.value.capitalize(), accumulate=False, parent=self + centre_point=pixel_centre, + basis_x=XAXIS, + dx=pixel_width, + basis_y=YAXIS, + dy=pixel_height, + slit=self._slit, + units=self._units.value.capitalize(), + accumulate=False, + parent=self, ) pixel_column.append(pixel) pixels.append(pixel_column) - return np.asarray(pixels, dtype='object') + return np.asarray(pixels, dtype="object") @property def height(self): @@ -851,9 +856,8 @@ def basis_y(self): @property def sightline_vectors(self): return np.asarray( - [[pixel.centre_point.vector_to(self._slit.centre_point) for pixel in pixel_column] - for pixel_column in self.pixels_as_foils], - dtype='object' + [[pixel.centre_point.vector_to(self._slit.centre_point) for pixel in pixel_column] for pixel_column in self.pixels_as_foils], + dtype="object", ) @property @@ -876,8 +880,7 @@ def units(self, units): self._units = _Units.RADIANCE else: raise ValueError( - "The units property of BolometerIRVB must be one of {}" - .format([member.value for member in _Units.__members__]) + "The units property of BolometerIRVB must be one of {}".format([member.value for member in _Units.__members__]) ) pipeline_class = self._PIPELINES[self._units] pipeline = pipeline_class(accumulate=self.accumulate) @@ -904,7 +907,7 @@ def as_sightlines(self): """ pixels = self.pixels_as_foils sightlines = [[pixel.as_sightline() for pixel in pixel_column] for pixel_column in pixels] - return np.asarray(sightlines, dtype='object') + return np.asarray(sightlines, dtype="object") def trace_sightlines(self): """ @@ -918,7 +921,7 @@ def trace_sightlines(self): """ pixels = self.pixels_as_foils traces = [[pixel.trace_sightline() for pixel in pixel_column] for pixel_column in pixels] - return np.asarray(traces, dtype='object') + return np.asarray(traces, dtype="object") def calculate_sensitivity(self, voxel_collection, ray_count=None): r""" @@ -1043,26 +1046,24 @@ def mask_corners(element): # Make the elements to cut out from the cover slightly thicker than the # cover, to guard against rounding errors - long_box = Box(lower=Point3D(-dx/2 + rc, -dy/2, -0.5 * dz), - upper=Point3D(dx/2 - rc, dy/2, 1.5 * dz)) - shot_box = Box(lower=Point3D(-dx/2, -dy/2 + rc, -0.5 * dz), - upper=Point3D(dx/2, dy/2 - rc, 1.5 * dz)) + long_box = Box(lower=Point3D(-dx / 2 + rc, -dy / 2, -0.5 * dz), upper=Point3D(dx / 2 - rc, dy / 2, 1.5 * dz)) + shot_box = Box(lower=Point3D(-dx / 2, -dy / 2 + rc, -0.5 * dz), upper=Point3D(dx / 2, dy / 2 - rc, 1.5 * dz)) cylinder_template = Cylinder(radius=rc, height=2 * dz) top_left_cylinder = cylinder_template.instance() - top_left_cylinder.transform = translate(-dx/2 + rc, dy/2 - rc, -dz/2) + top_left_cylinder.transform = translate(-dx / 2 + rc, dy / 2 - rc, -dz / 2) top_right_cylinder = cylinder_template.instance() - top_right_cylinder.transform = translate(dx/2 - rc, dy/2 - rc, -dz/2) + top_right_cylinder.transform = translate(dx / 2 - rc, dy / 2 - rc, -dz / 2) bottom_right_cylinder = cylinder_template.instance() - bottom_right_cylinder.transform = translate(dx/2 - rc, -dy/2 + rc, -dz/2) + bottom_right_cylinder.transform = translate(dx / 2 - rc, -dy / 2 + rc, -dz / 2) bottom_left_cylinder = cylinder_template.instance() - bottom_left_cylinder.transform = translate(-dx/2 + rc, -dy/2 + rc, -dz/2) - cutout = functools.reduce(Union, (long_box, shot_box, top_left_cylinder, - top_right_cylinder, bottom_right_cylinder, - bottom_left_cylinder)) - cover = Box(lower=Point3D(-dx/2, -dy/2, 0), upper=Point3D(dx/2, dy/2, dz)) + bottom_left_cylinder.transform = translate(-dx / 2 + rc, -dy / 2 + rc, -dz / 2) + cutout = functools.reduce( + Union, (long_box, shot_box, top_left_cylinder, top_right_cylinder, bottom_right_cylinder, bottom_left_cylinder) + ) + cover = Box(lower=Point3D(-dx / 2, -dy / 2, 0), upper=Point3D(dx / 2, dy / 2, dz)) mask = Subtract(cover, cutout) mask.material = AbsorbingSurface() mask.transform = translate(0, 0, dz) - mask.name = element.name + ' - rounded edges mask' + mask.name = element.name + " - rounded edges mask" mask.parent = element diff --git a/cherab/tools/observers/group/__init__.py b/cherab/tools/observers/group/__init__.py index eca93585..8a24f80f 100644 --- a/cherab/tools/observers/group/__init__.py +++ b/cherab/tools/observers/group/__init__.py @@ -18,6 +18,6 @@ from .fibreoptic import FibreOpticGroup from .sightline import SightLineGroup -from .targettedpixel import TargettedPixelGroup +from .targetedpixel import TargetedPixelGroup from .pixel import PixelGroup from .spectroscopic import SpectroscopicFibreOpticGroup, SpectroscopicSightLineGroup diff --git a/cherab/tools/observers/group/targettedpixel.py b/cherab/tools/observers/group/targettedpixel.py index e440f70e..a9f8775e 100644 --- a/cherab/tools/observers/group/targettedpixel.py +++ b/cherab/tools/observers/group/targettedpixel.py @@ -22,11 +22,11 @@ from .base import Observer0DGroup -class TargettedPixelGroup(Observer0DGroup): +class TargetedPixelGroup(Observer0DGroup): """ A group of targeted pixel under a single scene-graph node. - A scene-graph object regrouping a series of 'TargettedPixel' + A scene-graph object regrouping a series of `TargetedPixel` observers as a scene-graph parent. Allows combined observation and display control simultaneously. @@ -49,8 +49,9 @@ def x_width(self, value): for pixel, v in zip(self._observers, value): pixel.x_width = v else: - raise ValueError("The length of 'x_width' ({}) " - "mismatches the number of pixels ({}).".format(len(value), len(self._observers))) + raise ValueError( + "The length of 'x_width' ({}) mismatches the number of pixels ({}).".format(len(value), len(self._observers)) + ) else: for pixel in self._observers: pixel.x_width = value @@ -66,8 +67,9 @@ def y_width(self, value): for pixel, v in zip(self._observers, value): pixel.y_width = v else: - raise ValueError("The length of 'y_width' ({}) " - "mismatches the number of pixels ({}).".format(len(value), len(self._observers))) + raise ValueError( + "The length of 'y_width' ({}) mismatches the number of pixels ({}).".format(len(value), len(self._observers)) + ) else: for pixel in self._observers: pixel.y_width = value @@ -92,8 +94,11 @@ def targets(self, value): for pixel, v in zip(self._observers, value): pixel.targets = v else: - raise ValueError("The number of provided target lists' ({}) " - "mismatches the number of pixels ({}).".format(len(value), len(self._observers))) + raise ValueError( + "The number of provided target lists' ({}) mismatches the number of pixels ({}).".format( + len(value), len(self._observers) + ) + ) else: # assuming a list of primitives, the pixel's setter will throw an error if not for pixel in self._observers: @@ -110,7 +115,9 @@ def targeted_path_prob(self, value): for pixel, v in zip(self._observers, value): pixel.targeted_path_prob = v else: - raise ValueError("The length of 'value' ({}) mismatches the number of pixels ({}).".format(len(value), len(self._observers))) + raise ValueError( + "The length of 'value' ({}) mismatches the number of pixels ({}).".format(len(value), len(self._observers)) + ) else: for pixel in self._observers: pixel.targeted_path_prob = value diff --git a/cherab/tools/tests/test_observer_groups.py b/cherab/tools/tests/test_observer_groups.py index a04f095c..f92ab7d3 100644 --- a/cherab/tools/tests/test_observer_groups.py +++ b/cherab/tools/tests/test_observer_groups.py @@ -1,11 +1,11 @@ import unittest from raysect.core.workflow import RenderEngine -from raysect.optical.observer import Observer0D, SightLine, FibreOptic, Pixel, TargetedPixel, PowerPipeline0D, SpectralPowerPipeline0D +from raysect.optical.observer import FibreOptic, Observer0D, Pixel, PowerPipeline0D, SightLine, SpectralPowerPipeline0D, TargetedPixel from raysect.primitive import Sphere +from cherab.tools.observers.group import FibreOpticGroup, PixelGroup, SightLineGroup, TargetedPixelGroup from cherab.tools.observers.group.base import Observer0DGroup -from cherab.tools.observers.group import SightLineGroup, FibreOpticGroup, PixelGroup, TargettedPixelGroup from cherab.tools.raytransfer import pipelines @@ -20,13 +20,13 @@ def setUp(self): def test_get_item(self): """Tests all inputs for the __get_item__ method""" group = self._GROUP_CLASS(observers=self.observers) - names = ['zero', 'one', 'two'] + names = ["zero", "one", "two"] group.names = names idx = slice(1, 3, 1) for observer, input_observer in zip(group[idx], self.observers[idx]): self.assertIs(observer, input_observer) - + for i, name in enumerate(names): self.assertIs(group[name], self.observers[i]) @@ -37,11 +37,11 @@ def test_get_item(self): group[1.2] with self.assertRaises(ValueError): - group['fail'] + group["fail"] - group.names = ['fail'] * len(group) + group.names = ["fail"] * len(group) with self.assertRaises(ValueError): - group['fail'] + group["fail"] def test_assignments(self): """Test assignments of all supported attributes of Observer0DGroup""" @@ -49,7 +49,7 @@ def test_assignments(self): group.observers = self.observers for grouped_observer, input_observer in zip(group.observers, self.observers): - self.assertIs(grouped_observer, input_observer, msg='Observers do not match') + self.assertIs(grouped_observer, input_observer, msg="Observers do not match") with self.assertRaises(ValueError): group.observers = [Sphere()] @@ -58,32 +58,32 @@ def test_assignments(self): group.observers = Sphere() # names - names = ['zero', 'one', 'two'] + names = ["zero", "one", "two"] group.names = names for grouped_observer, input_name in zip(group.observers, names): - self.assertEqual(grouped_observer.name, input_name, msg='Observer name do not match') + self.assertEqual(grouped_observer.name, input_name, msg="Observer name do not match") with self.assertRaises(ValueError): - group.names = ['fail'] + group.names = ["fail"] with self.assertRaises(TypeError): - group.names = 'fail' + group.names = "fail" # pipelines - ppln_0 = PowerPipeline0D(name='pipeline zero, observer zero') - ppln_1 = PowerPipeline0D(name='pipeline one, observer one') - ppln_2 = PowerPipeline0D(name='pipeline two, observer two') - ppln_3 = PowerPipeline0D(name='pipeline three, observer two') + ppln_0 = PowerPipeline0D(name="pipeline zero, observer zero") + ppln_1 = PowerPipeline0D(name="pipeline one, observer one") + ppln_2 = PowerPipeline0D(name="pipeline two, observer two") + ppln_3 = PowerPipeline0D(name="pipeline three, observer two") pipelist = [[ppln_0], [ppln_1], [ppln_2, ppln_3]] group.pipelines = pipelist - self.assertIs(group[0].pipelines[0], ppln_0, 'non matching pipeline') - self.assertIs(group[1].pipelines[0], ppln_1, 'non matching pipeline') - self.assertIs(group[2].pipelines[0], ppln_2, 'non matching pipeline') - self.assertIs(group[2].pipelines[1], ppln_3, 'non matching pipeline') + self.assertIs(group[0].pipelines[0], ppln_0, "non matching pipeline") + self.assertIs(group[1].pipelines[0], ppln_1, "non matching pipeline") + self.assertIs(group[2].pipelines[0], ppln_2, "non matching pipeline") + self.assertIs(group[2].pipelines[1], ppln_3, "non matching pipeline") with self.assertRaises(ValueError): group.pipelines = [ppln_0] - # render_engine + # render_engine engine = RenderEngine() group.render_engine = engine for group_engine in group.render_engine: @@ -102,15 +102,15 @@ def test_assignments(self): with self.assertRaises(ValueError): group.render_engine = [RenderEngine() for _ in range(len(group) - 1)] - # wavelengths + # wavelengths wvl = 500 group.min_wavelength = wvl - 100 group.max_wavelength = wvl + 100 self.assertListEqual(group.min_wavelength, [wvl - 100] * len(group)) self.assertListEqual(group.max_wavelength, [wvl + 100] * len(group)) - min_wvls = [90 + 10*i for i in range(len(group))] - max_wvls = [100 + 10*i for i in range(len(group))] + min_wvls = [90 + 10 * i for i in range(len(group))] + max_wvls = [100 + 10 * i for i in range(len(group))] group.min_wavelength = min_wvls group.max_wavelength = max_wvls self.assertListEqual(group.min_wavelength, min_wvls) @@ -122,7 +122,7 @@ def test_assignments(self): group.min_wavelength = [90] * (len(group) - 1) # spectral - bins = [200 + i*100 for i in range(len(group))] + bins = [200 + i * 100 for i in range(len(group))] rays = [2] * len(group) group.spectral_bins = bins group.spectral_rays = rays @@ -139,7 +139,7 @@ def test_assignments(self): with self.assertRaises(ValueError): group.spectral_bins = [1000] * (len(group) + 1) - # quiet + # quiet quiet = [True] * len(group) group.quiet = quiet self.assertListEqual(group.quiet, quiet) @@ -152,8 +152,8 @@ def test_assignments(self): with self.assertRaises(ValueError): group.quiet = [False] * (len(group) + 1) - # rays - probs = [0.2 + i*0.1 for i in range(len(group))] + # rays + probs = [0.2 + i * 0.1 for i in range(len(group))] max_depths = [5 + i for i in range(len(group))] min_depths = [2 + i for i in range(len(group))] sampling = [False] * len(group) @@ -196,10 +196,10 @@ def test_assignments(self): group.ray_importance_sampling = [False] * (len(group) + 1) with self.assertRaises(ValueError): group.ray_important_path_weight = [0.7] * (len(group) + 1) - + # samples - pixel_samples = [2000 + i*500 for i in range(len(group))] - per_task = [5000 + i*100 for i in range(len(group))] + pixel_samples = [2000 + i * 500 for i in range(len(group))] + per_task = [5000 + i * 100 for i in range(len(group))] group.pixel_samples = pixel_samples group.samples_per_task = per_task self.assertListEqual(group.pixel_samples, pixel_samples) @@ -228,7 +228,7 @@ def test_connect_pipelines(self): group = self._GROUP_CLASS(observers=self.observers) ppln_classes = [PowerPipeline0D, SpectralPowerPipeline0D] - names = ['power', 'spectral'] + names = ["power", "spectral"] keywords = [ dict(name=names[0]), dict(name=names[1], display_progress=True), @@ -348,8 +348,8 @@ def test_widths(self): group.y_width = [1e-1] * (len(group) + 1) -class TargettedPixelGroupTestCase(PixelGroupTestCase): - _GROUP_CLASS = TargettedPixelGroup +class TargetedPixelGroupTestCase(PixelGroupTestCase): + _GROUP_CLASS = TargetedPixelGroup def setUp(self): self.observers = [TargetedPixel(targets=[Sphere()], pipelines=[PowerPipeline0D()]) for _ in range(self._NUM)] @@ -374,7 +374,7 @@ def test_targets(self): with self.assertRaises(ValueError): group.targets = targets - # targetted path prob + # targeted path prob prob = [0.9, 0.95, 1] group.targeted_path_prob = prob self.assertListEqual(group.targeted_path_prob, prob) diff --git a/docs/source/tools/observers.rst b/docs/source/tools/observers.rst index e93e2057..84f94f48 100644 --- a/docs/source/tools/observers.rst +++ b/docs/source/tools/observers.rst @@ -109,10 +109,10 @@ combined into a group. Group observers --------------- -Group observer is a collection of observers of the same type. All Observer0D classes -defined in Raysect are supoorted. The parameters of individual observers in a group +Group observer is a collection of observers of the same type. All Observer0D classes +defined in Raysect are supoorted. The parameters of individual observers in a group may differ. Group observer allows combined observation, namely, calling the observe -function for a group leads to a sequential call of this function for each observer +function for a group leads to a sequential call of this function for each observer in the group. .. autoclass:: cherab.tools.observers.group.base.Observer0DGroup @@ -127,7 +127,7 @@ in the group. .. autoclass:: cherab.tools.observers.group.PixelGroup :members: -.. autoclass:: cherab.tools.observers.group.TargettedPixelGroup +.. autoclass:: cherab.tools.observers.group.TargetedPixelGroup :members: Spectroscopic Groups @@ -136,9 +136,9 @@ Spectroscopic Groups .. deprecated:: 1.4.0 Use groups based on Raysect's observer classes instead -These groups take control of spectroscopic lines of sight observers. They support -direction and origin positioning and contain methods for plotting the power and -spectrum. Originally, these were called group observers and did not include the +These groups take control of spectroscopic lines of sight observers. They support +direction and origin positioning and contain methods for plotting the power and +spectrum. Originally, these were called group observers and did not include the Spectroscopic prefix in class name. .. autoclass:: cherab.tools.observers.SpectroscopicSightLine From 662eb0447fda4fd80a925a1b804d2a19b77c28ab Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Mon, 13 Oct 2025 11:45:35 +0200 Subject: [PATCH 05/23] Rename filename --- .../tools/observers/group/{targettedpixel.py => targetedpixel.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cherab/tools/observers/group/{targettedpixel.py => targetedpixel.py} (100%) diff --git a/cherab/tools/observers/group/targettedpixel.py b/cherab/tools/observers/group/targetedpixel.py similarity index 100% rename from cherab/tools/observers/group/targettedpixel.py rename to cherab/tools/observers/group/targetedpixel.py From af3d5c2bc660362eec30d193d9b2d8eaf2ecb40c Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Mon, 13 Oct 2025 11:54:45 +0200 Subject: [PATCH 06/23] Fix numpy v2 error related thing --- cherab/tools/tests/test_voxels.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cherab/tools/tests/test_voxels.py b/cherab/tools/tests/test_voxels.py index 046fffbe..62b4fcc6 100644 --- a/cherab/tools/tests/test_voxels.py +++ b/cherab/tools/tests/test_voxels.py @@ -280,8 +280,7 @@ def test_rectangle_area(self): for rectangle in RECTANGULAR_VOXEL_COORDS: coords = np.asarray(rectangle) voxel = AxisymmetricVoxel(coords) - dx = coords[:, 0].ptp() - dy = coords[:, 1].ptp() + dx, dy = np.ptp(coords, axis=0) expected_area = dx * dy self.assertEqual(voxel.cross_sectional_area, expected_area) From 3d53694233ece459c60b258eb73827f2f3de4ef2 Mon Sep 17 00:00:00 2001 From: Koyo MUNECHIKA <51052381+munechika-koyo@users.noreply.github.com> Date: Mon, 13 Oct 2025 12:06:57 +0200 Subject: [PATCH 07/23] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73fcdbd9..25e2467d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install Python dependencies - run: python -m pip install --prefer-binary cython~=3.1 numpy>=2 scipy matplotlib "pyopencl[pocl]>=2022.2.4" + run: python -m pip install --prefer-binary setuptools cython~=3.1 numpy>=2 scipy matplotlib "pyopencl[pocl]>=2022.2.4" - name: Install Raysect from pypi run: pip install raysect==0.9.* - name: Build cherab From 58708e0337f6ac38a47c7ff10764408ce924d10c Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Mon, 13 Oct 2025 13:15:00 +0200 Subject: [PATCH 08/23] Remove Python 3.14 from CI matrix until pyopencl support is available --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25e2467d..77b05087 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] #, "3.14"] # TODO: re-enable 3.14 when pyopencl supports it steps: - name: Checkout code uses: actions/checkout@v2 From 1a63b6d35a21a5af5cda4a7a76efe6d37e6efe21 Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Tue, 14 Oct 2025 17:21:15 +0200 Subject: [PATCH 09/23] Update CHANGELOG.md for Release 1.6.0: add API changes and new features --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f22a5d4..6a4b7e88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,15 @@ Project Changelog Release 1.6.0 (TBD) ------------------- +API changes: +* Rename 'targetted' to 'targeted' following Raysect change. (#486) New: * Add Function6D framework. (#478) * Add e_field attribute to Plasma object for electric field vector. (#465) * Add Integrator2D base class for integration of two-dimensional functions. (#472) +* Support Raysect 0.9. (#486) +* Test against Python 3.9, 3.10, 3.11, 3.12, 3.13 and latest released Numpy. Drop Python 3.7, 3.8 and older Numpy from tests. (#486) Release 1.5.0 (27 Aug 2024) ------------------- @@ -134,7 +138,7 @@ API changes: New: * Merged cherab-openadas package into the core cherab package to simplify installation. -* Beam object uses a cone primitive instead of a cylinder for the bounding volume of divergent beams. +* Beam object uses a cone primitive instead of a cylinder for the bounding volume of divergent beams. * Added Clamp functions. * Added ThermalCXRate. * Added optimised ray transfer grid calculation tools. @@ -162,7 +166,7 @@ New: Bug fixes: * Improved handling on non c-order arrays in various methods. -* Numerous minor bug fixes (see commit history) +* Numerous minor bug fixes (see commit history) Release 1.0.1 (1 Oct 2018) From fde76c4daa79bf37c20c98411940c761f65e9c34 Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Thu, 16 Oct 2025 10:00:28 +0200 Subject: [PATCH 10/23] Revert "Fix remaining typo `targetted` to `targeted`" This reverts commit 10d690feb7abc365acbe4383a17bd1699957f14c. --- cherab/core/model/laser/profile.pyx | 60 +++--- cherab/tools/observers/__init__.py | 14 +- cherab/tools/observers/bolometry.py | 201 +++++++++--------- cherab/tools/observers/group/__init__.py | 2 +- cherab/tools/observers/group/targetedpixel.py | 25 +-- cherab/tools/tests/test_observer_groups.py | 70 +++--- docs/source/tools/observers.rst | 14 +- 7 files changed, 186 insertions(+), 200 deletions(-) diff --git a/cherab/core/model/laser/profile.pyx b/cherab/core/model/laser/profile.pyx index 80b73d02..82376980 100644 --- a/cherab/core/model/laser/profile.pyx +++ b/cherab/core/model/laser/profile.pyx @@ -4,7 +4,7 @@ from raysect.primitive import Cylinder from raysect.optical cimport Spectrum, Vector3D, translate from cherab.core.laser cimport Laser, LaserProfile -from cherab.core.model.laser.math_functions cimport ConstantAxisymmetricGaussian3D, ConstantBivariateGaussian3D, TrivariateGaussian3D, GaussianBeamModel +from cherab.core.model.laser.math_functions cimport ConstantAxisymmetricGaussian3D, ConstantBivariateGaussian3D, TrivariateGaussian3D, GaussianBeamModel from cherab.core.utility.constants cimport SPEED_OF_LIGHT @@ -22,20 +22,20 @@ cdef class UniformEnergyDensity(LaserProfile): The methods get_pointing, get_polarization and get_energy_density are not limited to the inside of the laser cylinder. If called alone for position (x, y, z) outisde the laser cylinder, they will still return non-zero values. - + In the following example, a laser of length of 2 m (extending from z=0 to z=2 m) with a radius of 3 cm and volumetric energy density of 5 J*m^-3 and polarisation in the y direction is created: .. code-block:: pycon - + >>> from raysect.core import Vector3D >>> from cherab.core.model.laser import UniformEnergyDensity - + >>> energy = 5 # energy density in J >>> radius = 3e-2 # laser radius in m >>> length = 2 # laser length in m >>> polarisation = Vector3D(0, 1, 0) # polarisation direction - + # create the laser profile >>> laser_profile = UniformEnergyDensity(energy, radius, length, polarisation) @@ -108,7 +108,7 @@ cdef class UniformEnergyDensity(LaserProfile): cpdef list generate_geometry(self): return generate_segmented_cylinder(self.laser_radius, self.laser_length) - + cdef class ConstantBivariateGaussian(LaserProfile): """ @@ -120,8 +120,8 @@ cdef class ConstantBivariateGaussian(LaserProfile): The model imitates a laser beam with a uniform power output within a single pulse. This results in the distribution of the energy density along the propagation direction of the laser (z-axis) to be also uniform. The integral value of laser energy Exy in an x-y plane is given by - - .. math:: + + .. math:: E_{xy} = \\frac{E_p}{(c * \\tau)}, where Ep is the energy of the laser pulse, tau is the temporal pulse length and c is the speed of light in vacuum. @@ -133,23 +133,23 @@ cdef class ConstantBivariateGaussian(LaserProfile): The sigma_x and sigma_y are standard deviations in x and y directions, respectively. .. note:: - The height of the cylinder, forming the laser beam, is given by the laser_length and is independent from the + The height of the cylinder, forming the laser beam, is given by the laser_length and is independent from the temporal length of the laser pulse given by pulse_length. This gives the possibility to independently control the size of the laser primitive and the value of the volumetric energy density. - + The methods get_pointing, get_polarization and get_energy_density are not limited to the inside of the laser cylinder. If called for position (x, y, z) outisde the laser cylinder, they can still return non-zero values. - + The following example shows how to create a laser with sigma_x= 1 cm and sigma_y=2 cm, which makes the laser profile in x-y plane to be elliptical. The pulse energy is 5 J and the laser temporal pulse length is 10 ns: .. code-block:: pycon - + >>> from raysect.core import Vector3D >>> from cherab.core.model.laser import ConstantBivariateGaussian - + >>> radius = 3e-2 # laser radius in m >>> length = 2 # laser length in m >>> polarisation = Vector3D(0, 1, 0) # polarisation direction @@ -157,7 +157,7 @@ cdef class ConstantBivariateGaussian(LaserProfile): >>> pulse_length = 1e-8 # pulse length in s >>> width_x = 1e-2 # standard deviation in x direction in m >>> width_y = 2e-2 # standard deviation in y direction in m - + # create the laser profile >>> laser_profile = ConstantBivariateGaussian(pulse_energy, pulse_length, radius, length, width_x, width_y, polarisation) @@ -323,7 +323,7 @@ cdef class TrivariateGaussian(LaserProfile): The sigma_x and sigma_y are standard deviations in x and y directions, respectively, and E_p is the energy deliverd by laser in a single laser pulse. The mu_z is the mean of the distribution in the z direction and controls th position of the laser pulse along the z direction. - The standard deviation in z direction sigma_z is calculated from the pulse length tau_p, which is the + The standard deviation in z direction sigma_z is calculated from the pulse length tau_p, which is the standard deviation of the Gaussian distributed ouput power of the laser within a single pulse: .. math:: @@ -332,24 +332,24 @@ cdef class TrivariateGaussian(LaserProfile): The c stands for the speed of light in vacuum. .. note:: - The height of the cylinder, forming the laser beam, is given by the laser_length and is independent from the + The height of the cylinder, forming the laser beam, is given by the laser_length and is independent from the temporal length of the laser pulse given by pulse_length. This gives the possibility to independently control the size of the laser primitive and the value of the volumetric energy density. - + The methods get_pointing, get_polarization and get_energy_density are not limited to the inside of the laser cylinder. If called alone for position (x, y, z) outisde the laser cylinder, they can still return non-zero values. - + The following example shows how to create a laser with sigma_x = 1 cm and sigma_y = 2 cm, which makes the laser profile in an x-y plane to be elliptical. The pulse energy is 5 J and the laser temporal pulse length is 10 ns. The position of the laser pulse maximum mean_z is set to 0.5: .. code-block:: pycon - + >>> from raysect.core import Vector3D >>> from cherab.core.model.laser import ConstantBivariateGaussian - + >>> radius = 3e-2 # laser radius in m >>> length = 2 # laser length in m >>> polarisation = Vector3D(0, 1, 0) # polarisation direction @@ -358,7 +358,7 @@ cdef class TrivariateGaussian(LaserProfile): >>> pulse_z = 0.5 # position of the pulse mean >>> width_x = 1e-2 # standard deviation in x direction in m >>> width_y = 2e-2 # standard deviation in y direction in m - + # create the laser profile >>> laser_profile = ConstantBivariateGaussian(pulse_energy, pulse_length, pulse_z, radius, length, width_x, width_y, polarisation) @@ -512,7 +512,7 @@ cdef class TrivariateGaussian(LaserProfile): self._distribution = TrivariateGaussian3D(self._mean_z, self._stddev_x, self._stddev_y, self._stddev_z) - normalisation = self._pulse_energy + normalisation = self._pulse_energy function = normalisation * self._distribution self.set_energy_density_function(function) @@ -541,16 +541,16 @@ cdef class GaussianBeamAxisymmetric(LaserProfile): .. math:: z_R = \\frac{\\pi \\omega_0^2 n}{\\lambda_l} - + where the omega_0 is the standard deviation in the xy plane in the focal point (beam waist) and lambda_l is the central wavelength of the laser. The E_xy stand for the laser energy in an xy plane and is calculated as: - + .. math:: E_{xy} = \\frac{E_p}{(c * \\tau)}, where the E_p is the energy in a single laser pulse and tau is the temporal pulse length. - .. note:: + .. note:: For more information about the Gaussian beam model see https://en.wikipedia.org/wiki/Gaussian_beam The methods get_pointing, get_polarization and get_energy_density are not limited to the inside @@ -562,10 +562,10 @@ cdef class GaussianBeamAxisymmetric(LaserProfile): waist is z=50 cm. The laser wavelength is 1060 nm. .. code-block:: pycon - + >>> from raysect.core import Vector3D >>> from cherab.core.model.laser import GaussianBeamAxisymmetric - + >>> radius = 5e-2 # laser radius in m >>> length = 2 # laser length in m >>> polarisation = Vector3D(0, 1, 0) # polarisation direction @@ -576,7 +576,7 @@ cdef class GaussianBeamAxisymmetric(LaserProfile): >>> width_x = 1e-2 # standard deviation in x direction in m >>> width_y = 2e-2 # standard deviation in y direction in m >>> laser_wlen = 1060 # laser wavelength in nm - + # create the laser profile >>> laser_profile = GaussianBeamAxisymmetric(pulse_energy, pulse_length, length, radius, waist_z, waist_width, laser_wlen) @@ -738,7 +738,7 @@ def generate_segmented_cylinder(radius, length): Generates a segmented cylindrical laser geometry Approximates a long cylinder with a cylindrical segments to optimize - targeted and importance sampling. The height of a cylinder segments is roughly + targetted and importance sampling. The height of a cylinder segments is roughly 2 * cylinder radius. :return: List of cylinders @@ -761,5 +761,5 @@ def generate_segmented_cylinder(radius, length): geometry.append(segment) else: raise ValueError("Incorrect number of segments calculated.") - + return geometry \ No newline at end of file diff --git a/cherab/tools/observers/__init__.py b/cherab/tools/observers/__init__.py index 670dfce5..d134ef63 100644 --- a/cherab/tools/observers/__init__.py +++ b/cherab/tools/observers/__init__.py @@ -1,3 +1,4 @@ + # Copyright 2016-2018 Euratom # Copyright 2016-2018 United Kingdom Atomic Energy Authority # Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas @@ -16,15 +17,8 @@ # See the Licence for the specific language governing permissions and limitations # under the Licence. -from .bolometry import BolometerCamera, BolometerFoil, BolometerIRVB, BolometerSlit +from .bolometry import BolometerCamera, BolometerFoil, BolometerSlit, BolometerIRVB from .calcam import load_calcam_calibration -from .group import ( - FibreOpticGroup, - PixelGroup, - SightLineGroup, - SpectroscopicFibreOpticGroup, - SpectroscopicSightLineGroup, - TargetedPixelGroup, -) from .intersections import find_wall_intersection -from .spectroscopy import SpectroscopicFibreOptic, SpectroscopicSightLine +from .spectroscopy import SpectroscopicSightLine, SpectroscopicFibreOptic +from .group import PixelGroup, TargettedPixelGroup, SightLineGroup, FibreOpticGroup, SpectroscopicFibreOpticGroup, SpectroscopicSightLineGroup diff --git a/cherab/tools/observers/bolometry.py b/cherab/tools/observers/bolometry.py index 532d2826..5cd8c1a2 100644 --- a/cherab/tools/observers/bolometry.py +++ b/cherab/tools/observers/bolometry.py @@ -1,3 +1,4 @@ + # Copyright 2016-2018 Euratom # Copyright 2016-2018 United Kingdom Atomic Energy Authority # Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas @@ -16,32 +17,23 @@ # See the Licence for the specific language governing permissions and limitations # under the Licence. -import functools from enum import Enum - +import functools import numpy as np -from raysect.core import Node, Point3D, Primitive, Vector3D, World, rotate_basis, translate -from raysect.core import Ray as CoreRay -from raysect.core.math.sampler import RectangleSampler3D, TargetedHemisphereSampler -from raysect.optical.material import AbsorbingSurface -from raysect.optical.material.material import NullMaterial -from raysect.optical.observer import ( - PowerPipeline0D, - PowerPipeline2D, - RadiancePipeline0D, - RadiancePipeline2D, - SightLine, - SpectralPowerPipeline0D, - SpectralPowerPipeline2D, - SpectralRadiancePipeline0D, - SpectralRadiancePipeline2D, - TargetedCCDArray, - TargetedPixel, -) + +from raysect.core import Node, translate, rotate_basis, Point3D, Vector3D, Ray as CoreRay, Primitive, World +from raysect.core.math.sampler import TargetedHemisphereSampler, RectangleSampler3D from raysect.primitive import Box, Cylinder, Subtract, Union +from raysect.optical.observer import PowerPipeline0D, RadiancePipeline0D, \ + SpectralPowerPipeline0D, SpectralRadiancePipeline0D, SightLine, TargetedPixel +from raysect.optical.observer import PowerPipeline2D, RadiancePipeline2D, \ + SpectralPowerPipeline2D, SpectralRadiancePipeline2D, TargetedCCDArray +from raysect.optical.material.material import NullMaterial +from raysect.optical.material import AbsorbingSurface from cherab.tools.inversions.voxels import VoxelCollection + R_2_PI = 1 / (2 * np.pi) @@ -79,7 +71,8 @@ class BolometerCamera(Node): >>> camera = BolometerCamera(name="MyBolometer", parent=world) """ - def __init__(self, camera_geometry=None, parent=None, transform=None, name=""): + def __init__(self, camera_geometry=None, parent=None, transform=None, name=''): + super().__init__(parent=parent, transform=transform, name=name) self._foil_detectors = [] @@ -140,8 +133,12 @@ def foil_detectors(self): @foil_detectors.setter def foil_detectors(self, value): + if not isinstance(value, list): - raise TypeError("The foil_detectors attribute of BolometerCamera must be a list of BolometerFoils or BolometerIRVBs.") + raise TypeError( + "The foil_detectors attribute of BolometerCamera must be a list of " + "BolometerFoils or BolometerIRVBs." + ) # Prevent external changes being made to this list value = value.copy() @@ -151,8 +148,8 @@ def foil_detectors(self, value): "The foil_detectors attribute of BolometerCamera must be a list of " "BolometerFoil or BolometerIRVB objects. Value {} is not a BolometerFoil " "or BolometerIRVB.".format(foil_detector) - ) - if foil_detector.slit not in self._slits: + ) + if not foil_detector.slit in self._slits: self._slits.append(foil_detector.slit) foil_detector.parent = self @@ -170,9 +167,11 @@ def add_foil_detector(self, foil_detector): """ if not isinstance(foil_detector, (BolometerFoil, BolometerIRVB)): - raise TypeError("The foil_detector argument must be of type BolometerFoil or BolometerIRVB.") + raise TypeError( + "The foil_detector argument must be of type BolometerFoil or BolometerIRVB." + ) - if foil_detector.slit not in self._slits: + if not foil_detector.slit in self._slits: self._slits.append(foil_detector.slit) foil_detector.parent = self @@ -214,7 +213,7 @@ class BolometerSlit(Node): larger than the slit dx and dy, which can cause partial occlusion of nearby primitives. It also relies on no rays being launched with directions outside the solid angle of the aperture's bounding sphere: depending on the - foil-slit distance and slit size, and also the foil's targeted_path_prob, + foil-slit distance and slit size, and also the foil's targetted_path_prob, this may not be guaranteed. Supplying a proper mesh geometry for the camera is recommended instead of using a CSG aperture. @@ -256,7 +255,9 @@ class BolometerSlit(Node): >>> slit = BolometerSlit("slit", centre_point, basis_x, dx, basis_y, dy, parent=camera) """ - def __init__(self, slit_id, centre_point, basis_x, dx, basis_y, dy, dz=0.001, parent=None, csg_aperture=False, curvature_radius=0): + def __init__(self, slit_id, centre_point, basis_x, dx, basis_y, dy, dz=0.001, + parent=None, csg_aperture=False, curvature_radius=0): + # perform validation of input parameters if not isinstance(dx, (float, int)): @@ -273,9 +274,11 @@ def __init__(self, slit_id, centre_point, basis_x, dx, basis_y, dy, dz=0.001, pa raise TypeError("centre_point argument for BolometerSlit must be of type Point3D.") if not isinstance(curvature_radius, (float, int)): - raise TypeError("curvature_radius argument for BolometerSlit must be of type float/int.") + raise TypeError("curvature_radius argument for BolometerSlit " + "must be of type float/int.") if curvature_radius < 0: - raise ValueError("curvature_radius argument for BolometerSlit must not be negative.") + raise ValueError("curvature_radius argument for BolometerSlit " + "must not be negative.") if not isinstance(basis_x, Vector3D): raise TypeError("The basis vectors of BolometerSlit must be of type Vector3D.") @@ -297,14 +300,8 @@ def __init__(self, slit_id, centre_point, basis_x, dx, basis_y, dy, dz=0.001, pa super().__init__(parent=parent, transform=transform, name=slit_id) - self.target = Box( - lower=Point3D(-dx / 2 * 1.01, -dy / 2 * 1.01, -dz / 2), - upper=Point3D(dx / 2 * 1.01, dy / 2 * 1.01, dz / 2), - transform=None, - material=NullMaterial(), - parent=self, - name=slit_id + " - target", - ) + self.target = Box(lower=Point3D(-dx/2*1.01, -dy/2*1.01, -dz/2), upper=Point3D(dx/2*1.01, dy/2*1.01, dz/2), + transform=None, material=NullMaterial(), parent=self, name=slit_id+' - target') self._csg_aperture = None self.csg_aperture = csg_aperture @@ -335,14 +332,14 @@ def csg_aperture(self): @csg_aperture.setter def csg_aperture(self, value): + if value is True: width = max(self.dx, self.dy) - face = Box(Point3D(-width, -width, -self.dz / 2), Point3D(width, width, self.dz / 2)) - slit = Box( - lower=Point3D(-self.dx / 2, -self.dy / 2, -self.dz / 2 - self.dz * 0.1), - upper=Point3D(self.dx / 2, self.dy / 2, self.dz / 2 + self.dz * 0.1), - ) - self._csg_aperture = Subtract(face, slit, parent=self, material=AbsorbingSurface(), name=self.name + " - CSG Aperture") + face = Box(Point3D(-width, -width, -self.dz/2), Point3D(width, width, self.dz/2)) + slit = Box(lower=Point3D(-self.dx/2, -self.dy/2, -self.dz/2 - self.dz*0.1), + upper=Point3D(self.dx/2, self.dy/2, self.dz/2 + self.dz*0.1)) + self._csg_aperture = Subtract(face, slit, parent=self, + material=AbsorbingSurface(), name=self.name+' - CSG Aperture') else: if isinstance(self._csg_aperture, Primitive): @@ -406,9 +403,9 @@ class BolometerFoil(TargetedPixel): >>> detector = BolometerFoil("ch#1", centre_point, basis_x, dx, basis_y, dy, slit, parent=camera) """ - def __init__( - self, detector_id, centre_point, basis_x, dx, basis_y, dy, slit, parent=None, units="Power", accumulate=False, curvature_radius=0 - ): + def __init__(self, detector_id, centre_point, basis_x, dx, basis_y, dy, slit, + parent=None, units="Power", accumulate=False, curvature_radius=0): + # perform validation of input parameters if not isinstance(dx, (float, int)): @@ -428,9 +425,11 @@ def __init__( raise TypeError("centre_point argument for BolometerFoil must be of type Point3D.") if not isinstance(curvature_radius, (float, int)): - raise TypeError("curvature_radius argument for BolometerFoil must be of type float/int.") + raise TypeError("curvature_radius argument for BolometerFoil " + "must be of type float/int.") if curvature_radius < 0: - raise ValueError("curvature_radius argument for BolometerFoil must not be negative.") + raise ValueError("curvature_radius argument for BolometerFoil " + "must not be negative.") if not isinstance(basis_x, Vector3D): raise TypeError("The basis vectors of BolometerFoil must be of type Vector3D.") @@ -448,18 +447,9 @@ def __init__( translation = translate(centre_point.x, centre_point.y, centre_point.z) rotation = rotate_basis(normal_vec, basis_y) - super().__init__( - [slit.target], - targeted_path_prob=1.0, - pixel_samples=1000, - x_width=dx, - y_width=dy, - spectral_bins=1, - quiet=True, - parent=parent, - transform=translation * rotation, - name=detector_id, - ) + super().__init__([slit.target], targeted_path_prob=1.0, + pixel_samples=1000, x_width=dx, y_width=dy, spectral_bins=1, quiet=True, + parent=parent, transform=translation * rotation, name=detector_id) # Update pipeline based on units self.units = units @@ -540,13 +530,16 @@ def as_sightline(self): else: raise ValueError("The units argument of BolometerFoil must be one of 'Power' or 'Radiance'.") - los_observer = SightLine(pipelines=[pipeline], pixel_samples=1, quiet=True, parent=self, name=self.name) + los_observer = SightLine(pipelines=[pipeline], pixel_samples=1, quiet=True, + parent=self, name=self.name) los_observer.render_engine = self.render_engine los_observer.spectral_bins = self.spectral_bins los_observer.min_wavelength = self.min_wavelength los_observer.max_wavelength = self.max_wavelength # The observer's Z axis should be aligned along the line of sight vector - los_observer.transform = rotate_basis(self.sightline_vector.transform(self.to_local()), self.basis_y) + los_observer.transform = rotate_basis( + self.sightline_vector.transform(self.to_local()), self.basis_y + ) return los_observer @@ -567,6 +560,7 @@ def trace_sightline(self): direction = self.sightline_vector while True: + # Find the next intersection point of the ray with the world intersection = self.root.hit(CoreRay(origin, direction)) @@ -667,8 +661,8 @@ def calculate_etendue(self, ray_count=10000, batches=10, max_distance=1e999): # generate bounding sphere and convert to local coordinate system sphere = target.bounding_sphere() spheres = [(sphere.centre.transform(self.to_local()), sphere.radius, 1.0)] - # instance targeted pixel sampler to sample directions - targeted_sampler = TargetedHemisphereSampler(spheres) + # instance targetted pixel sampler to sample directions + targetted_sampler = TargetedHemisphereSampler(spheres) # instance rectangle pixel sampler to sample origins point_sampler = RectangleSampler3D(width=self.x_width, height=self.y_width) @@ -677,8 +671,8 @@ def etendue_single_run(_): origins = point_sampler(samples=ray_count) passed = 0.0 for origin in origins: - # obtain targeted vector sample - direction, pdf = targeted_sampler(origin, pdf=True) + # obtain targetted vector sample + direction, pdf = targetted_sampler(origin, pdf=True) path_weight = R_2_PI * direction.z / pdf # Transform to world space origin = origin.transform(detector_transform) @@ -764,10 +758,14 @@ class BolometerIRVB(TargetedCCDArray): >>> detector = BolometerIRVB("irvb", width, pixels, slit, transform, parent=camera) """ - _PIPELINES = {_Units.POWER: PowerPipeline2D, _Units.RADIANCE: RadiancePipeline2D} - _SPECTRAL_PIPELINES = {_Units.POWER: SpectralPowerPipeline2D, _Units.RADIANCE: SpectralRadiancePipeline2D} + _PIPELINES = {_Units.POWER: PowerPipeline2D, + _Units.RADIANCE: RadiancePipeline2D} + _SPECTRAL_PIPELINES = {_Units.POWER: SpectralPowerPipeline2D, + _Units.RADIANCE: SpectralRadiancePipeline2D} + + def __init__(self, name, width, pixels, slit, transform, parent=None, + units="power", accumulate=False, curvature_radius=0): - def __init__(self, name, width, pixels, slit, transform, parent=None, units="power", accumulate=False, curvature_radius=0): # perform validation of input parameters width = float(width) if width < 0: @@ -778,15 +776,16 @@ def __init__(self, name, width, pixels, slit, transform, parent=None, units="pow curvature_radius = float(curvature_radius) if curvature_radius < 0: - raise ValueError("curvature_radius argument for BolometerIRVB must not be negative.") + raise ValueError("curvature_radius argument for BolometerIRVB " + "must not be negative.") self._slit = slit self._curvature_radius = curvature_radius self._accumulate = None # Will be set after pipeline is created. - super().__init__( - [slit.target], pixels=pixels, width=width, targeted_path_prob=0.99, parent=parent, pipelines=[], transform=transform, name=name - ) + super().__init__([slit.target], pixels=pixels, width=width, + targeted_path_prob=0.99, parent=parent, pipelines=[], + transform=transform, name=name) self.pixel_samples = 1000 self.spectral_bins = 1 self.quiet = True @@ -816,22 +815,18 @@ def pixels_as_foils(self): for x in range(nx): pixel_column = [] for y in range(ny): - pixel_centre = foil_bottom_left + (x + 0.5) * XAXIS * pixel_width + (y + 0.5) * YAXIS * pixel_height + pixel_centre = (foil_bottom_left + + (x + 0.5) * XAXIS * pixel_width + + (y + 0.5) * YAXIS * pixel_height) pixel = BolometerFoil( detector_id="IRVB pixel ({},{})".format(x + 1, y + 1), - centre_point=pixel_centre, - basis_x=XAXIS, - dx=pixel_width, - basis_y=YAXIS, - dy=pixel_height, - slit=self._slit, - units=self._units.value.capitalize(), - accumulate=False, - parent=self, + centre_point=pixel_centre, basis_x=XAXIS, dx=pixel_width, + basis_y=YAXIS, dy=pixel_height, slit=self._slit, + units=self._units.value.capitalize(), accumulate=False, parent=self ) pixel_column.append(pixel) pixels.append(pixel_column) - return np.asarray(pixels, dtype="object") + return np.asarray(pixels, dtype='object') @property def height(self): @@ -856,8 +851,9 @@ def basis_y(self): @property def sightline_vectors(self): return np.asarray( - [[pixel.centre_point.vector_to(self._slit.centre_point) for pixel in pixel_column] for pixel_column in self.pixels_as_foils], - dtype="object", + [[pixel.centre_point.vector_to(self._slit.centre_point) for pixel in pixel_column] + for pixel_column in self.pixels_as_foils], + dtype='object' ) @property @@ -880,7 +876,8 @@ def units(self, units): self._units = _Units.RADIANCE else: raise ValueError( - "The units property of BolometerIRVB must be one of {}".format([member.value for member in _Units.__members__]) + "The units property of BolometerIRVB must be one of {}" + .format([member.value for member in _Units.__members__]) ) pipeline_class = self._PIPELINES[self._units] pipeline = pipeline_class(accumulate=self.accumulate) @@ -907,7 +904,7 @@ def as_sightlines(self): """ pixels = self.pixels_as_foils sightlines = [[pixel.as_sightline() for pixel in pixel_column] for pixel_column in pixels] - return np.asarray(sightlines, dtype="object") + return np.asarray(sightlines, dtype='object') def trace_sightlines(self): """ @@ -921,7 +918,7 @@ def trace_sightlines(self): """ pixels = self.pixels_as_foils traces = [[pixel.trace_sightline() for pixel in pixel_column] for pixel_column in pixels] - return np.asarray(traces, dtype="object") + return np.asarray(traces, dtype='object') def calculate_sensitivity(self, voxel_collection, ray_count=None): r""" @@ -1046,24 +1043,26 @@ def mask_corners(element): # Make the elements to cut out from the cover slightly thicker than the # cover, to guard against rounding errors - long_box = Box(lower=Point3D(-dx / 2 + rc, -dy / 2, -0.5 * dz), upper=Point3D(dx / 2 - rc, dy / 2, 1.5 * dz)) - shot_box = Box(lower=Point3D(-dx / 2, -dy / 2 + rc, -0.5 * dz), upper=Point3D(dx / 2, dy / 2 - rc, 1.5 * dz)) + long_box = Box(lower=Point3D(-dx/2 + rc, -dy/2, -0.5 * dz), + upper=Point3D(dx/2 - rc, dy/2, 1.5 * dz)) + shot_box = Box(lower=Point3D(-dx/2, -dy/2 + rc, -0.5 * dz), + upper=Point3D(dx/2, dy/2 - rc, 1.5 * dz)) cylinder_template = Cylinder(radius=rc, height=2 * dz) top_left_cylinder = cylinder_template.instance() - top_left_cylinder.transform = translate(-dx / 2 + rc, dy / 2 - rc, -dz / 2) + top_left_cylinder.transform = translate(-dx/2 + rc, dy/2 - rc, -dz/2) top_right_cylinder = cylinder_template.instance() - top_right_cylinder.transform = translate(dx / 2 - rc, dy / 2 - rc, -dz / 2) + top_right_cylinder.transform = translate(dx/2 - rc, dy/2 - rc, -dz/2) bottom_right_cylinder = cylinder_template.instance() - bottom_right_cylinder.transform = translate(dx / 2 - rc, -dy / 2 + rc, -dz / 2) + bottom_right_cylinder.transform = translate(dx/2 - rc, -dy/2 + rc, -dz/2) bottom_left_cylinder = cylinder_template.instance() - bottom_left_cylinder.transform = translate(-dx / 2 + rc, -dy / 2 + rc, -dz / 2) - cutout = functools.reduce( - Union, (long_box, shot_box, top_left_cylinder, top_right_cylinder, bottom_right_cylinder, bottom_left_cylinder) - ) - cover = Box(lower=Point3D(-dx / 2, -dy / 2, 0), upper=Point3D(dx / 2, dy / 2, dz)) + bottom_left_cylinder.transform = translate(-dx/2 + rc, -dy/2 + rc, -dz/2) + cutout = functools.reduce(Union, (long_box, shot_box, top_left_cylinder, + top_right_cylinder, bottom_right_cylinder, + bottom_left_cylinder)) + cover = Box(lower=Point3D(-dx/2, -dy/2, 0), upper=Point3D(dx/2, dy/2, dz)) mask = Subtract(cover, cutout) mask.material = AbsorbingSurface() mask.transform = translate(0, 0, dz) - mask.name = element.name + " - rounded edges mask" + mask.name = element.name + ' - rounded edges mask' mask.parent = element diff --git a/cherab/tools/observers/group/__init__.py b/cherab/tools/observers/group/__init__.py index 8a24f80f..eca93585 100644 --- a/cherab/tools/observers/group/__init__.py +++ b/cherab/tools/observers/group/__init__.py @@ -18,6 +18,6 @@ from .fibreoptic import FibreOpticGroup from .sightline import SightLineGroup -from .targetedpixel import TargetedPixelGroup +from .targettedpixel import TargettedPixelGroup from .pixel import PixelGroup from .spectroscopic import SpectroscopicFibreOpticGroup, SpectroscopicSightLineGroup diff --git a/cherab/tools/observers/group/targetedpixel.py b/cherab/tools/observers/group/targetedpixel.py index a9f8775e..e440f70e 100644 --- a/cherab/tools/observers/group/targetedpixel.py +++ b/cherab/tools/observers/group/targetedpixel.py @@ -22,11 +22,11 @@ from .base import Observer0DGroup -class TargetedPixelGroup(Observer0DGroup): +class TargettedPixelGroup(Observer0DGroup): """ A group of targeted pixel under a single scene-graph node. - A scene-graph object regrouping a series of `TargetedPixel` + A scene-graph object regrouping a series of 'TargettedPixel' observers as a scene-graph parent. Allows combined observation and display control simultaneously. @@ -49,9 +49,8 @@ def x_width(self, value): for pixel, v in zip(self._observers, value): pixel.x_width = v else: - raise ValueError( - "The length of 'x_width' ({}) mismatches the number of pixels ({}).".format(len(value), len(self._observers)) - ) + raise ValueError("The length of 'x_width' ({}) " + "mismatches the number of pixels ({}).".format(len(value), len(self._observers))) else: for pixel in self._observers: pixel.x_width = value @@ -67,9 +66,8 @@ def y_width(self, value): for pixel, v in zip(self._observers, value): pixel.y_width = v else: - raise ValueError( - "The length of 'y_width' ({}) mismatches the number of pixels ({}).".format(len(value), len(self._observers)) - ) + raise ValueError("The length of 'y_width' ({}) " + "mismatches the number of pixels ({}).".format(len(value), len(self._observers))) else: for pixel in self._observers: pixel.y_width = value @@ -94,11 +92,8 @@ def targets(self, value): for pixel, v in zip(self._observers, value): pixel.targets = v else: - raise ValueError( - "The number of provided target lists' ({}) mismatches the number of pixels ({}).".format( - len(value), len(self._observers) - ) - ) + raise ValueError("The number of provided target lists' ({}) " + "mismatches the number of pixels ({}).".format(len(value), len(self._observers))) else: # assuming a list of primitives, the pixel's setter will throw an error if not for pixel in self._observers: @@ -115,9 +110,7 @@ def targeted_path_prob(self, value): for pixel, v in zip(self._observers, value): pixel.targeted_path_prob = v else: - raise ValueError( - "The length of 'value' ({}) mismatches the number of pixels ({}).".format(len(value), len(self._observers)) - ) + raise ValueError("The length of 'value' ({}) mismatches the number of pixels ({}).".format(len(value), len(self._observers))) else: for pixel in self._observers: pixel.targeted_path_prob = value diff --git a/cherab/tools/tests/test_observer_groups.py b/cherab/tools/tests/test_observer_groups.py index f92ab7d3..a04f095c 100644 --- a/cherab/tools/tests/test_observer_groups.py +++ b/cherab/tools/tests/test_observer_groups.py @@ -1,11 +1,11 @@ import unittest from raysect.core.workflow import RenderEngine -from raysect.optical.observer import FibreOptic, Observer0D, Pixel, PowerPipeline0D, SightLine, SpectralPowerPipeline0D, TargetedPixel +from raysect.optical.observer import Observer0D, SightLine, FibreOptic, Pixel, TargetedPixel, PowerPipeline0D, SpectralPowerPipeline0D from raysect.primitive import Sphere -from cherab.tools.observers.group import FibreOpticGroup, PixelGroup, SightLineGroup, TargetedPixelGroup from cherab.tools.observers.group.base import Observer0DGroup +from cherab.tools.observers.group import SightLineGroup, FibreOpticGroup, PixelGroup, TargettedPixelGroup from cherab.tools.raytransfer import pipelines @@ -20,13 +20,13 @@ def setUp(self): def test_get_item(self): """Tests all inputs for the __get_item__ method""" group = self._GROUP_CLASS(observers=self.observers) - names = ["zero", "one", "two"] + names = ['zero', 'one', 'two'] group.names = names idx = slice(1, 3, 1) for observer, input_observer in zip(group[idx], self.observers[idx]): self.assertIs(observer, input_observer) - + for i, name in enumerate(names): self.assertIs(group[name], self.observers[i]) @@ -37,11 +37,11 @@ def test_get_item(self): group[1.2] with self.assertRaises(ValueError): - group["fail"] + group['fail'] - group.names = ["fail"] * len(group) + group.names = ['fail'] * len(group) with self.assertRaises(ValueError): - group["fail"] + group['fail'] def test_assignments(self): """Test assignments of all supported attributes of Observer0DGroup""" @@ -49,7 +49,7 @@ def test_assignments(self): group.observers = self.observers for grouped_observer, input_observer in zip(group.observers, self.observers): - self.assertIs(grouped_observer, input_observer, msg="Observers do not match") + self.assertIs(grouped_observer, input_observer, msg='Observers do not match') with self.assertRaises(ValueError): group.observers = [Sphere()] @@ -58,32 +58,32 @@ def test_assignments(self): group.observers = Sphere() # names - names = ["zero", "one", "two"] + names = ['zero', 'one', 'two'] group.names = names for grouped_observer, input_name in zip(group.observers, names): - self.assertEqual(grouped_observer.name, input_name, msg="Observer name do not match") + self.assertEqual(grouped_observer.name, input_name, msg='Observer name do not match') with self.assertRaises(ValueError): - group.names = ["fail"] + group.names = ['fail'] with self.assertRaises(TypeError): - group.names = "fail" + group.names = 'fail' # pipelines - ppln_0 = PowerPipeline0D(name="pipeline zero, observer zero") - ppln_1 = PowerPipeline0D(name="pipeline one, observer one") - ppln_2 = PowerPipeline0D(name="pipeline two, observer two") - ppln_3 = PowerPipeline0D(name="pipeline three, observer two") + ppln_0 = PowerPipeline0D(name='pipeline zero, observer zero') + ppln_1 = PowerPipeline0D(name='pipeline one, observer one') + ppln_2 = PowerPipeline0D(name='pipeline two, observer two') + ppln_3 = PowerPipeline0D(name='pipeline three, observer two') pipelist = [[ppln_0], [ppln_1], [ppln_2, ppln_3]] group.pipelines = pipelist - self.assertIs(group[0].pipelines[0], ppln_0, "non matching pipeline") - self.assertIs(group[1].pipelines[0], ppln_1, "non matching pipeline") - self.assertIs(group[2].pipelines[0], ppln_2, "non matching pipeline") - self.assertIs(group[2].pipelines[1], ppln_3, "non matching pipeline") + self.assertIs(group[0].pipelines[0], ppln_0, 'non matching pipeline') + self.assertIs(group[1].pipelines[0], ppln_1, 'non matching pipeline') + self.assertIs(group[2].pipelines[0], ppln_2, 'non matching pipeline') + self.assertIs(group[2].pipelines[1], ppln_3, 'non matching pipeline') with self.assertRaises(ValueError): group.pipelines = [ppln_0] - # render_engine + # render_engine engine = RenderEngine() group.render_engine = engine for group_engine in group.render_engine: @@ -102,15 +102,15 @@ def test_assignments(self): with self.assertRaises(ValueError): group.render_engine = [RenderEngine() for _ in range(len(group) - 1)] - # wavelengths + # wavelengths wvl = 500 group.min_wavelength = wvl - 100 group.max_wavelength = wvl + 100 self.assertListEqual(group.min_wavelength, [wvl - 100] * len(group)) self.assertListEqual(group.max_wavelength, [wvl + 100] * len(group)) - min_wvls = [90 + 10 * i for i in range(len(group))] - max_wvls = [100 + 10 * i for i in range(len(group))] + min_wvls = [90 + 10*i for i in range(len(group))] + max_wvls = [100 + 10*i for i in range(len(group))] group.min_wavelength = min_wvls group.max_wavelength = max_wvls self.assertListEqual(group.min_wavelength, min_wvls) @@ -122,7 +122,7 @@ def test_assignments(self): group.min_wavelength = [90] * (len(group) - 1) # spectral - bins = [200 + i * 100 for i in range(len(group))] + bins = [200 + i*100 for i in range(len(group))] rays = [2] * len(group) group.spectral_bins = bins group.spectral_rays = rays @@ -139,7 +139,7 @@ def test_assignments(self): with self.assertRaises(ValueError): group.spectral_bins = [1000] * (len(group) + 1) - # quiet + # quiet quiet = [True] * len(group) group.quiet = quiet self.assertListEqual(group.quiet, quiet) @@ -152,8 +152,8 @@ def test_assignments(self): with self.assertRaises(ValueError): group.quiet = [False] * (len(group) + 1) - # rays - probs = [0.2 + i * 0.1 for i in range(len(group))] + # rays + probs = [0.2 + i*0.1 for i in range(len(group))] max_depths = [5 + i for i in range(len(group))] min_depths = [2 + i for i in range(len(group))] sampling = [False] * len(group) @@ -196,10 +196,10 @@ def test_assignments(self): group.ray_importance_sampling = [False] * (len(group) + 1) with self.assertRaises(ValueError): group.ray_important_path_weight = [0.7] * (len(group) + 1) - + # samples - pixel_samples = [2000 + i * 500 for i in range(len(group))] - per_task = [5000 + i * 100 for i in range(len(group))] + pixel_samples = [2000 + i*500 for i in range(len(group))] + per_task = [5000 + i*100 for i in range(len(group))] group.pixel_samples = pixel_samples group.samples_per_task = per_task self.assertListEqual(group.pixel_samples, pixel_samples) @@ -228,7 +228,7 @@ def test_connect_pipelines(self): group = self._GROUP_CLASS(observers=self.observers) ppln_classes = [PowerPipeline0D, SpectralPowerPipeline0D] - names = ["power", "spectral"] + names = ['power', 'spectral'] keywords = [ dict(name=names[0]), dict(name=names[1], display_progress=True), @@ -348,8 +348,8 @@ def test_widths(self): group.y_width = [1e-1] * (len(group) + 1) -class TargetedPixelGroupTestCase(PixelGroupTestCase): - _GROUP_CLASS = TargetedPixelGroup +class TargettedPixelGroupTestCase(PixelGroupTestCase): + _GROUP_CLASS = TargettedPixelGroup def setUp(self): self.observers = [TargetedPixel(targets=[Sphere()], pipelines=[PowerPipeline0D()]) for _ in range(self._NUM)] @@ -374,7 +374,7 @@ def test_targets(self): with self.assertRaises(ValueError): group.targets = targets - # targeted path prob + # targetted path prob prob = [0.9, 0.95, 1] group.targeted_path_prob = prob self.assertListEqual(group.targeted_path_prob, prob) diff --git a/docs/source/tools/observers.rst b/docs/source/tools/observers.rst index 84f94f48..e93e2057 100644 --- a/docs/source/tools/observers.rst +++ b/docs/source/tools/observers.rst @@ -109,10 +109,10 @@ combined into a group. Group observers --------------- -Group observer is a collection of observers of the same type. All Observer0D classes -defined in Raysect are supoorted. The parameters of individual observers in a group +Group observer is a collection of observers of the same type. All Observer0D classes +defined in Raysect are supoorted. The parameters of individual observers in a group may differ. Group observer allows combined observation, namely, calling the observe -function for a group leads to a sequential call of this function for each observer +function for a group leads to a sequential call of this function for each observer in the group. .. autoclass:: cherab.tools.observers.group.base.Observer0DGroup @@ -127,7 +127,7 @@ in the group. .. autoclass:: cherab.tools.observers.group.PixelGroup :members: -.. autoclass:: cherab.tools.observers.group.TargetedPixelGroup +.. autoclass:: cherab.tools.observers.group.TargettedPixelGroup :members: Spectroscopic Groups @@ -136,9 +136,9 @@ Spectroscopic Groups .. deprecated:: 1.4.0 Use groups based on Raysect's observer classes instead -These groups take control of spectroscopic lines of sight observers. They support -direction and origin positioning and contain methods for plotting the power and -spectrum. Originally, these were called group observers and did not include the +These groups take control of spectroscopic lines of sight observers. They support +direction and origin positioning and contain methods for plotting the power and +spectrum. Originally, these were called group observers and did not include the Spectroscopic prefix in class name. .. autoclass:: cherab.tools.observers.SpectroscopicSightLine From 97a634cbfa97602f1dd7ab00449464a2f395a8ac Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Thu, 16 Oct 2025 10:00:57 +0200 Subject: [PATCH 11/23] Revert "Rename filename" This reverts commit 662eb0447fda4fd80a925a1b804d2a19b77c28ab. --- .../tools/observers/group/{targetedpixel.py => targettedpixel.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cherab/tools/observers/group/{targetedpixel.py => targettedpixel.py} (100%) diff --git a/cherab/tools/observers/group/targetedpixel.py b/cherab/tools/observers/group/targettedpixel.py similarity index 100% rename from cherab/tools/observers/group/targetedpixel.py rename to cherab/tools/observers/group/targettedpixel.py From 60d76bff22f57f3ed6ca1c6db5cc3df624a163ed Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Thu, 16 Oct 2025 10:15:16 +0200 Subject: [PATCH 12/23] Fix typo: change `targetted` to `targeted` in the internal code of Bolometer classes --- cherab/tools/observers/bolometry.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cherab/tools/observers/bolometry.py b/cherab/tools/observers/bolometry.py index 5cd8c1a2..b69fef29 100644 --- a/cherab/tools/observers/bolometry.py +++ b/cherab/tools/observers/bolometry.py @@ -213,7 +213,7 @@ class BolometerSlit(Node): larger than the slit dx and dy, which can cause partial occlusion of nearby primitives. It also relies on no rays being launched with directions outside the solid angle of the aperture's bounding sphere: depending on the - foil-slit distance and slit size, and also the foil's targetted_path_prob, + foil-slit distance and slit size, and also the foil's targeted_path_prob, this may not be guaranteed. Supplying a proper mesh geometry for the camera is recommended instead of using a CSG aperture. @@ -661,8 +661,8 @@ def calculate_etendue(self, ray_count=10000, batches=10, max_distance=1e999): # generate bounding sphere and convert to local coordinate system sphere = target.bounding_sphere() spheres = [(sphere.centre.transform(self.to_local()), sphere.radius, 1.0)] - # instance targetted pixel sampler to sample directions - targetted_sampler = TargetedHemisphereSampler(spheres) + # instance targeted pixel sampler to sample directions + targeted_sampler = TargetedHemisphereSampler(spheres) # instance rectangle pixel sampler to sample origins point_sampler = RectangleSampler3D(width=self.x_width, height=self.y_width) @@ -671,8 +671,8 @@ def etendue_single_run(_): origins = point_sampler(samples=ray_count) passed = 0.0 for origin in origins: - # obtain targetted vector sample - direction, pdf = targetted_sampler(origin, pdf=True) + # obtain targeted vector sample + direction, pdf = targeted_sampler(origin, pdf=True) path_weight = R_2_PI * direction.z / pdf # Transform to world space origin = origin.transform(detector_transform) From ab0b0fd51577a37b05e36f90798de517580be2e5 Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Thu, 16 Oct 2025 10:17:15 +0200 Subject: [PATCH 13/23] Bump version to 1.6.0.dev2 --- cherab/core/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cherab/core/VERSION b/cherab/core/VERSION index 4c39f7c4..8a3469e1 100644 --- a/cherab/core/VERSION +++ b/cherab/core/VERSION @@ -1 +1 @@ -1.6.0.dev1 +1.6.0.dev2 From 3dbad49df1a15d309e130eaa87358706e6fb0bba Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Thu, 16 Oct 2025 10:20:38 +0200 Subject: [PATCH 14/23] Fix typo: change `targetted` to `targeted` in generate_segmented_cylinder function --- cherab/core/model/laser/profile.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cherab/core/model/laser/profile.pyx b/cherab/core/model/laser/profile.pyx index 82376980..ed5f6024 100644 --- a/cherab/core/model/laser/profile.pyx +++ b/cherab/core/model/laser/profile.pyx @@ -738,7 +738,7 @@ def generate_segmented_cylinder(radius, length): Generates a segmented cylindrical laser geometry Approximates a long cylinder with a cylindrical segments to optimize - targetted and importance sampling. The height of a cylinder segments is roughly + targeted and importance sampling. The height of a cylinder segments is roughly 2 * cylinder radius. :return: List of cylinders From 503aa22fb2d270948b7227505d698119b0804255 Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Thu, 16 Oct 2025 11:28:54 +0200 Subject: [PATCH 15/23] Restore `targetted_path_prob` property --- cherab/tools/observers/group/targettedpixel.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cherab/tools/observers/group/targettedpixel.py b/cherab/tools/observers/group/targettedpixel.py index e440f70e..f21694aa 100644 --- a/cherab/tools/observers/group/targettedpixel.py +++ b/cherab/tools/observers/group/targettedpixel.py @@ -33,7 +33,7 @@ class TargettedPixelGroup(Observer0DGroup): :ivar list x_width: Width of pixel along local x axis :ivar list y_width: Width of pixel along local y axis :ivar list targets: Targets for preferential sampling - :ivar list targeted_path_prob: Probability of ray being casted at the target + :ivar list targetted_path_prob: Probability of ray being casted at the target """ _OBSERVER_TYPE = TargetedPixel @@ -100,11 +100,11 @@ def targets(self, value): pixel.targets = value @property - def targeted_path_prob(self): + def targetted_path_prob(self): return [pixel.targeted_path_prob for pixel in self._observers] - @targeted_path_prob.setter - def targeted_path_prob(self, value): + @targetted_path_prob.setter + def targetted_path_prob(self, value): if isinstance(value, (list, tuple)): if len(value) == len(self._observers): for pixel, v in zip(self._observers, value): From 5182d8b7a56955ee8704e36fb0316bec93462bf8 Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Thu, 16 Oct 2025 11:31:17 +0200 Subject: [PATCH 16/23] Restore `targetted_path_prob` in test --- cherab/tools/tests/test_observer_groups.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/cherab/tools/tests/test_observer_groups.py b/cherab/tools/tests/test_observer_groups.py index a04f095c..c75e8630 100644 --- a/cherab/tools/tests/test_observer_groups.py +++ b/cherab/tools/tests/test_observer_groups.py @@ -26,7 +26,7 @@ def test_get_item(self): idx = slice(1, 3, 1) for observer, input_observer in zip(group[idx], self.observers[idx]): self.assertIs(observer, input_observer) - + for i, name in enumerate(names): self.assertIs(group[name], self.observers[i]) @@ -83,7 +83,7 @@ def test_assignments(self): with self.assertRaises(ValueError): group.pipelines = [ppln_0] - # render_engine + # render_engine engine = RenderEngine() group.render_engine = engine for group_engine in group.render_engine: @@ -102,7 +102,7 @@ def test_assignments(self): with self.assertRaises(ValueError): group.render_engine = [RenderEngine() for _ in range(len(group) - 1)] - # wavelengths + # wavelengths wvl = 500 group.min_wavelength = wvl - 100 group.max_wavelength = wvl + 100 @@ -139,7 +139,7 @@ def test_assignments(self): with self.assertRaises(ValueError): group.spectral_bins = [1000] * (len(group) + 1) - # quiet + # quiet quiet = [True] * len(group) group.quiet = quiet self.assertListEqual(group.quiet, quiet) @@ -152,7 +152,7 @@ def test_assignments(self): with self.assertRaises(ValueError): group.quiet = [False] * (len(group) + 1) - # rays + # rays probs = [0.2 + i*0.1 for i in range(len(group))] max_depths = [5 + i for i in range(len(group))] min_depths = [2 + i for i in range(len(group))] @@ -196,7 +196,7 @@ def test_assignments(self): group.ray_importance_sampling = [False] * (len(group) + 1) with self.assertRaises(ValueError): group.ray_important_path_weight = [0.7] * (len(group) + 1) - + # samples pixel_samples = [2000 + i*500 for i in range(len(group))] per_task = [5000 + i*100 for i in range(len(group))] @@ -376,13 +376,13 @@ def test_targets(self): # targetted path prob prob = [0.9, 0.95, 1] - group.targeted_path_prob = prob - self.assertListEqual(group.targeted_path_prob, prob) + group.targetted_path_prob = prob + self.assertListEqual(group.targetted_path_prob, prob) prob = 0.8 group.targeted_path_prob = prob - for group_targeted_path_prob in group.targeted_path_prob: + for group_targeted_path_prob in group.targetted_path_prob: self.assertEqual(group_targeted_path_prob, prob) with self.assertRaises(ValueError): - group.targeted_path_prob = [0.7] * (len(group) + 1) + group.targetted_path_prob = [0.7] * (len(group) + 1) From 362a6e3b8ba190a0e389aaaffbef7b5ed3674da9 Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Thu, 16 Oct 2025 11:34:43 +0200 Subject: [PATCH 17/23] Remove API changes section --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a4b7e88..417bd613 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,6 @@ Project Changelog Release 1.6.0 (TBD) ------------------- -API changes: -* Rename 'targetted' to 'targeted' following Raysect change. (#486) New: * Add Function6D framework. (#478) From 21d64497f1a46ae1d330e76bbcafffb6084ebcfb Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Thu, 16 Oct 2025 11:45:57 +0200 Subject: [PATCH 18/23] Fix typo --- cherab/tools/tests/test_observer_groups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cherab/tools/tests/test_observer_groups.py b/cherab/tools/tests/test_observer_groups.py index c75e8630..0418eab9 100644 --- a/cherab/tools/tests/test_observer_groups.py +++ b/cherab/tools/tests/test_observer_groups.py @@ -380,7 +380,7 @@ def test_targets(self): self.assertListEqual(group.targetted_path_prob, prob) prob = 0.8 - group.targeted_path_prob = prob + group.targetted_path_prob = prob for group_targeted_path_prob in group.targetted_path_prob: self.assertEqual(group_targeted_path_prob, prob) From 9b20d374dbf1e88aa8f87e8ade416b7b249f9cb8 Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Fri, 17 Oct 2025 18:01:15 +0200 Subject: [PATCH 19/23] Revert import lines in setup.py --- setup.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index 8699c4f5..15bc291a 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,14 @@ -import multiprocessing +from collections import defaultdict +import sys import os import os.path as path -import sys -from collections import defaultdict from pathlib import Path - +import multiprocessing import numpy +from setuptools import setup, find_packages, Extension from Cython.Build import cythonize -from setuptools import Extension, find_packages, setup -multiprocessing.set_start_method("fork") +multiprocessing.set_start_method('fork') force = False profile = False From 780364f40251fd652dcf3e95c495a7d251871714 Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Fri, 17 Oct 2025 18:01:51 +0200 Subject: [PATCH 20/23] Update numpy version requirement to 2.0 in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 15bc291a..c57bb0d3 100644 --- a/setup.py +++ b/setup.py @@ -117,7 +117,7 @@ long_description=long_description, long_description_content_type="text/markdown", install_requires=[ - "numpy>=2", + "numpy>=2.0", "scipy", "matplotlib", "raysect==0.9.1.*", From cae511476ff9450494bcc70f841ad6e3a8be53d9 Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Fri, 17 Oct 2025 18:02:29 +0200 Subject: [PATCH 21/23] Update numpy version requirement to 2.0 in requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7fea14d1..c99e710f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ cython~=3.1 -numpy>=2 +numpy>=2.0 scipy matplotlib raysect==0.9.1.* From 8d48d541c15bf51761e79796904cf5983a17a136 Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Mon, 27 Oct 2025 09:42:22 +0100 Subject: [PATCH 22/23] Add deprecation warnings for `targetted_path_prob` property in Bolometer classes --- cherab/tools/observers/bolometry.py | 37 +++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/cherab/tools/observers/bolometry.py b/cherab/tools/observers/bolometry.py index b69fef29..33870b0a 100644 --- a/cherab/tools/observers/bolometry.py +++ b/cherab/tools/observers/bolometry.py @@ -18,6 +18,7 @@ # under the Licence. from enum import Enum +from warnings import warn import functools import numpy as np @@ -516,6 +517,24 @@ def accumulate(self, value): # Discard any samples from previous accumulate behaviour pipeline.value.clear() + @property + def targetted_path_prob(self): + warn( + "The 'targetted_path_prob' property is deprecated, use 'targeted_path_prob' instead.", + DeprecationWarning, + stacklevel=2 + ) + return self._targeted_path_prob + + @targetted_path_prob.setter + def targetted_path_prob(self, value): + warn( + "The 'targetted_path_prob' property is deprecated, use 'targeted_path_prob' instead.", + DeprecationWarning, + stacklevel=2 + ) + self.targeted_path_prob = value + def as_sightline(self): """ Constructs a SightLine observer for this bolometer. @@ -896,6 +915,24 @@ def accumulate(self, value): if pipeline.frame is not None: pipeline.frame.clear() + @property + def targetted_path_prob(self): + warn( + "The 'targetted_path_prob' property is deprecated, use 'targeted_path_prob' instead.", + DeprecationWarning, + stacklevel=2, + ) + return self._targeted_path_prob + + @targetted_path_prob.setter + def targetted_path_prob(self, value): + warn( + "The 'targetted_path_prob' property is deprecated, use 'targeted_path_prob' instead.", + DeprecationWarning, + stacklevel=2, + ) + self.targeted_path_prob = value + def as_sightlines(self): """ Constructs a SightLine observer for each pixel in this bolometer. From 5810951d0af0ece11a0789341783d038b9d5ab1a Mon Sep 17 00:00:00 2001 From: munechika-koyo Date: Mon, 3 Nov 2025 10:19:05 +0100 Subject: [PATCH 23/23] Re-enable Python 3.14 in CI workflow matrix --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77b05087..25e2467d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] #, "3.14"] # TODO: re-enable 3.14 when pyopencl supports it + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Checkout code uses: actions/checkout@v2