Source code for honeybee_radiance.luminaire

"""Module for simulating electric lighting."""
import os
import io
import math

from ladybug_geometry.geometry3d import Vector3D, Point3D
from honeybee_radiance_command.ies2rad import Ies2rad, Ies2radOptions
from honeybee_radiance.config import folders


[docs] class Luminaire(object): """A luminaire defined by an IES photometric file. The Luminaire object stores the photometric data of a light fixture and provides methods to: - Parse IES LM-63 photometric data - Generate Radiance geometry via ies2rad - Generate pure-Python photometric web geometry - Place and orient luminaires in space via a LuminaireZone Args: ies_path: Path to an IES LM-63 photometric file. The file will be read and stored internally as text to ensure the Luminaire can be serialized and recreated even if the original file is no longer available. identifier: Optional text string for a unique Luminaire ID. This identifier is used to name Radiance output files and geometry. If None, the identifier will be derived from the IES file name. luminaire_zone: Optional LuminaireZone object defining the spatial placement, orientation, and repetition of the luminaire in the scene. This is required to generate positioned Radiance geometry (xform output). light_loss_factor: A scalar multiplier applied to account for lamp lumen depreciation, dirt depreciation, or other system losses. Must be greater than 0. Default: 1. candela_multiplier: Additional scalar multiplier applied to candela values after parsing the IES file. Must be greater than 0. Default: 1. Properties: * ies_path * ies_content * identifier * luminaire_zone * custom_lamp * light_loss_factor * candela_multiplier * vertical_angles * horizontal_angles * candela_values * unit_type * width * length * height * max_candela * unit_scale """ __slots__ = ('_ies_path', '_ies_content', '_identifier', '_luminaire_zone', '_custom_lamp', '_light_loss_factor', '_candela_multiplier', '_vertical_angles', '_horizontal_angles', '_candela_values', '_unit_type', '_width', '_length', '_height', '_max_candela', '_unit_scale') def __init__(self, ies_path, identifier=None, luminaire_zone=None, custom_lamp=None, light_loss_factor=1, candela_multiplier=1): self.ies_path = ies_path self.identifier = identifier self.luminaire_zone = luminaire_zone self.custom_lamp = custom_lamp self.light_loss_factor = light_loss_factor self.candela_multiplier = candela_multiplier self._unit_type = None self._unit_scale = None self._vertical_angles = None self._horizontal_angles = None self._candela_values = None self._width = None self._length = None self._height = None self._max_candela = None
[docs] @classmethod def from_dict(cls, data): """Create a Luminaire from a dictionary. Args: data: A python dictionary of a Luminaire. """ assert data['type'] == 'Luminaire', \ 'Expected Luminaire dictionary. Got {}'.format(data['type']) if data['ies_path'] is None: ies_path = data['ies_content'] else: ies_path = data['ies_path'] new_obj = cls( ies_path, identifier=data.get('identifier', None), light_loss_factor=data.get('light_loss_factor', 1), candela_multiplier=data.get('candela_multiplier', 1) ) new_obj.ies_content = data['ies_content'] if data.get('luminaire_zone') is not None: new_obj.luminaire_zone = LuminaireZone.from_dict( data['luminaire_zone'] ) if data.get('custom_lamp') is not None: new_obj.custom_lamp = CustomLamp.from_dict( data['custom_lamp'] ) return new_obj
@property def ies_path(self): """Get or set the IES file of the luminaire.""" return self._ies_path @ies_path.setter def ies_path(self, ies_input): if ies_input is None: raise ValueError('ies_path cannot be None or empty.') # file path if isinstance(ies_input, str) and os.path.isfile(ies_input): self._ies_path = ies_input with io.open(ies_input, 'r', encoding='utf-8', errors='ignore') as f: self._ies_content = f.read() return # IES content as a string if isinstance(ies_input, str): text = ies_input.lstrip() if text.upper().startswith('IESNA'): self._ies_path = None self._ies_content = ies_input return raise IOError( 'ies_path must be a valid file path or IES file content as a string.' ) @property def ies_content(self): """Get or set the IES content of the luminaire.""" return self._ies_content @ies_content.setter def ies_content(self, content): self._ies_content = content @property def identifier(self): """Get or set the identifier of the luminaire.""" return self._identifier @identifier.setter def identifier(self, value): if value is None: if self._ies_path: value = os.path.splitext( os.path.basename(self._ies_path) )[0] else: value = self._identifier_from_ies_content() if not value: value = 'luminaire' self._identifier = value @property def luminaire_zone(self): """Get or set the LuminaireZone of the luminaire.""" return self._luminaire_zone @luminaire_zone.setter def luminaire_zone(self, zone): if zone is not None: assert isinstance(zone, LuminaireZone), \ 'Expected LuminaireZone object or None. Got {}'.format(type(zone)) self._luminaire_zone = zone @property def custom_lamp(self): """Get or set the custom lamp of the luminaire.""" return self._custom_lamp @custom_lamp.setter def custom_lamp(self, lamp): if lamp is not None: assert isinstance(lamp, CustomLamp), \ 'Expected CustomLamp object or None. Got {}'.format(type(lamp)) self._custom_lamp = lamp @property def light_loss_factor(self): """Get or set the light loss factor (default = 1).""" return self._light_loss_factor @light_loss_factor.setter def light_loss_factor(self, value): if value is None: value = 1 value = float(value) if value <= 0: raise ValueError('light_loss_factor must be greater than 0.') self._light_loss_factor = value @property def candela_multiplier(self): """Get or set the candela multiplier (default = 1).""" return self._candela_multiplier @candela_multiplier.setter def candela_multiplier(self, value): if value is None: value = 1 value = float(value) if value <= 0: raise ValueError('candela_multiplier must be greater than 0.') self._candela_multiplier = value @property def unit_type(self): """IES unit type: 1=feet, 2=meters""" return self._unit_type @property def unit_scale(self): """Scale factor to convert IES units to meters""" return self._unit_scale @property def candela_values(self): """Raw candela values from IES""" self._ensure_parsed() return self._candela_values @property def normalized_candela_values(self): """Candela values normalized by max candela""" self._ensure_parsed() if self._max_candela == 0: return self._candela_values return [ [v / self._max_candela for v in row] for row in self._candela_values ] @property def vertical_angles(self): """Get the list of vertical angles defined in the IES file.""" return self._vertical_angles @property def horizontal_angles(self): """Get the list of horizontal angles defined in the IES file.""" return self._horizontal_angles @property def width(self): """Get the luminaire width.""" return self._width @property def length(self): """Get the luminaire length.""" return self._length @property def height(self): """Get the luminaire height.""" return self._height @property def max_candela(self): """Get the maximum candela value in the photometric distribution.""" return self._max_candela @property def width_m(self): """Get the luminaire width in meters.""" self._ensure_parsed() return self.width * self.unit_scale @property def length_m(self): """Get the luminaire length in meters.""" self._ensure_parsed() return self.length * self.unit_scale @property def height_m(self): """Get the luminaire height in meters.""" self._ensure_parsed() return self.height * self.unit_scale
[docs] def ies2rad(self, libdir=None, prefdir=None, outname=None): """Executes ies2rad. Args: libdir: Set the library directory. prefdir: Set the library subdirectory. outname: Output file name root. Returns: Radiance scene description (rad file). """ ies_path = self._ensure_ies_file(folder=prefdir) command = Ies2rad(ies=ies_path) options = Ies2radOptions() outname = outname or self.identifier options.o = outname options.d = 'm' multiplier = self.light_loss_factor * self.candela_multiplier if self.custom_lamp is not None: multiplier *= self.custom_lamp.depreciation_factor if self.custom_lamp.is_white: tab_dir = prefdir or '.' if not os.path.exists(tab_dir): os.makedirs(tab_dir) tab_name = os.path.join(tab_dir, '{}.tab'.format(outname)).replace('\\', '/') if not os.path.isabs(tab_name) and not tab_name.startswith('.'): tab_name = os.path.join('.', tab_name) x, y = self.custom_lamp.white_xy with open(tab_name, 'w') as f: f.write('/{}/ {} {} {}\n'.format(self.custom_lamp.name, x, y, self.custom_lamp.depreciation_factor)) options.t = self.custom_lamp.name options.f = tab_name elif self.custom_lamp.is_rgb: options.t = 'default' options.c = self.custom_lamp.rgb else: raise ValueError('CustomLamp must be either white or RGB.') if multiplier != 1: options.m = multiplier if libdir: libdir = os.path.normpath(libdir).replace('\\', '/') if not os.path.isabs(libdir) and not libdir.startswith('.'): libdir = './' + libdir options.l = libdir if prefdir: prefdir = os.path.normpath(prefdir).replace('\\', '/') if libdir and prefdir.startswith('./'): prefdir = prefdir[2:] elif not libdir and not prefdir.startswith('.'): prefdir = './' + prefdir if not prefdir.startswith('.'): prefdir = './' + prefdir options.p = prefdir rad_path = os.path.join(prefdir or '.', '{}.rad'.format(outname)).replace('\\', '/') if not os.path.isabs(rad_path) and not rad_path.startswith('.'): rad_path = './' + rad_path command.options = options env = dict(os.environ, **folders.env) if folders.env else None command.run(env=env) return rad_path
[docs] def generate_scene(self, libdir=None, prefdir=None): """Create a combined scene description of LuminaireZone and Luminaire. This method will create a scene description where there scene from ies2rad is added in the correct location via xform. Args: libdir: Set the library directory. prefdir: Set the library subdirectory. Return: Combined Radiance scene description of LuminaireZone and Luminaire. """ assert self.luminaire_zone is not None, 'Luminaire zone is required to generate scene.' luminaire_rad_path = self.ies2rad( libdir=libdir, prefdir=prefdir, outname='__{}__'.format(self.identifier) ) scene_dir = os.path.dirname(luminaire_rad_path) or '.' scene_path = os.path.join(scene_dir, '{}.rad'.format(self.identifier)).replace('\\', '/') if not scene_path.startswith('.'): scene_path = './' + scene_path with open(scene_path, 'w') as f: for inst in self.luminaire_zone.instances: px, py, pz = inst.point spin, tilt, rotation = inst.spin, inst.tilt, inst.rotation f.write('!xform -rz {} -ry {} -rz {} -t {} {} {} {}\n'.format( spin, tilt, rotation, px, py, pz, luminaire_rad_path )) return scene_path
[docs] def write_ies(self, folder, filename=None): """Write the stored IES content back to disk.""" filename = filename or '{}.ies'.format(self.identifier) path = os.path.join(folder, filename) with open(path, 'w', encoding='utf-8') as f: f.write(self._ies_content) return path
[docs] def parse_photometric_data(self): """Parse IES LM-63 photometric data from stored IES content. Populates: - vertical_angles - horizontal_angles - candela_values - unit_type - width, length, height - max_candela """ if not self._ies_content: raise RuntimeError('No IES content to parse.') lines = self._ies_content.splitlines() data_started = False tokens = [] for line in lines: line = line.strip() if not line: continue if line.upper().startswith('IESNA'): continue if line.startswith('['): continue if not data_started: parts = line.split() try: float(parts[0]) data_started = True except Exception: continue if data_started: line = [float(v) for v in line.split()] tokens.extend(line) if not tokens: raise RuntimeError('Failed to parse numeric IES data.') data = {} data['number_of_lamps'] = int(tokens[0]) data['lumens_per_lamp'] = tokens[1] data['ies_candela_multiplier'] = tokens[2] data['num_vertical_angles'] = int(tokens[3]) data['num_horizontal_angles'] = int(tokens[4]) data['photometric_type'] = int(tokens[5]) data['unit_type'] = int(tokens[6]) data['width'] = tokens[7] data['length'] = tokens[8] data['height'] = tokens[9] data['ballast_factor'] = tokens[10] data['future_use'] = tokens[11] data['input_watts'] = tokens[12] if data['photometric_type'] != 1: raise ValueError( 'Only Type C photometry is supported (got {}).' .format(data['photometric_type']) ) idx = 13 data['vertical_angles'] = tokens[idx:idx+data['num_vertical_angles']] idx += data['num_vertical_angles'] data['horizontal_angles'] = tokens[idx:idx+data['num_horizontal_angles']] idx += data['num_horizontal_angles'] candela_values = [] for h in range(data['num_horizontal_angles']): row = tokens[idx:idx+data['num_vertical_angles']] idx += data['num_vertical_angles'] candela_values.append(row) if data['ies_candela_multiplier'] != 1: candela_values = [ [v * data['ies_candela_multiplier'] for v in row] for row in candela_values ] data['candela_values'] = candela_values data['max_candela'] = max( (max(row) for row in candela_values if row) ) self._vertical_angles = data['vertical_angles'] self._horizontal_angles = data['horizontal_angles'] self._candela_values = data['candela_values'] self._width = data['width'] self._length = data['length'] self._height = data['height'] self._max_candela = data['max_candela'] if data['unit_type'] == 1: data['unit_scale'] = 0.3048 elif data['unit_type'] == 2: data['unit_scale'] = 1.0 else: raise ValueError( 'Unsupported unit type in IES file: {}'.format(data['unit_type']) ) self._unit_type = data['unit_type'] self._unit_scale = data['unit_scale']
[docs] def generate_photometric_web(self, normalize=True): """Generate a photometric web geometry. Args: normalize: If set to True the geometry is normalized to unit dimensions. Returns: dict with keys: - points: list of Point3D objects. - horizontal_angles (rad) - vertical_angles (rad) - scale """ self._ensure_parsed() horz_deg, candela = self._expand_horizontal_angles( self.horizontal_angles, self.candela_values ) vert_deg = self.vertical_angles horz = [math.radians(h) for h in horz_deg] vert = [math.radians(v) for v in vert_deg] if normalize: max_cd = self.max_candela or 1.0 candela = [ [v / max_cd for v in row] for row in candela ] scale = max(abs(self.width_m), abs(self.length_m)) points = [] for h_idx, h_ang in enumerate(horz): row = [] for v_idx, v_ang in enumerate(vert): cd = scale * candela[h_idx][v_idx] x = cd * math.sin(v_ang) * math.cos(h_ang) y = cd * math.sin(v_ang) * math.sin(h_ang) z = -cd * math.cos(v_ang) row.append(Point3D(x, y, z)) points.append(row) return { 'points': points, 'horizontal_angles': horz, 'vertical_angles': vert, 'scale': scale }
def _expand_horizontal_angles(self, horizontal_angles, candela_values): """Expand IES horizontal symmetry to full 0-360 coverage. Args: horizontal_angles: A list of horizontal angles. candela_values: A list of candela values. Returns: Tuple with (horizontal angles, candela values). """ horz = list(horizontal_angles) candelas = [list(row) for row in candela_values] if len(horz) == 1: horz = list(range(0, 361, 10)) candelas = candelas * len(horz) return horz, candelas counter = 0 while horz[0] == 0 and horz[-1] < 360: counter += 1 intervals = [ horz[-1] - horz[-idx - 2] for idx in range(len(horz) - 1) ] new_angles = [ horz[-1] + intervals[idx - 1] for idx in range(1, len(horz)) ] horz.extend(new_angles) candelas.extend(list(reversed(candelas))[1:]) if counter > 4: raise ValueError('Horizontal angles are not symmetric or ordered.') if horz[0] > 0: zerolimit = [ horz[0] - (horz[idx] - horz[0]) for idx in range(1, len(horz)) if horz[0] - (horz[idx] - horz[0]) >= 0 ][::-1] maxlimit = [ horz[-1] + (horz[-1] - horz[-1 - idx]) for idx in range(1, len(horz)) if horz[-1] + (horz[-1] - horz[-1 - idx]) <= 360 ] c0 = candelas[1:len(zerolimit) + 1][::-1] c1 = candelas[-len(maxlimit) - 1:-1][::-1] horz = zerolimit + horz + maxlimit candelas = c0 + candelas + c1 return horz, candelas def _ensure_parsed(self): if self._vertical_angles is None: self.parse_photometric_data() def _ensure_ies_file(self, folder=None): """Ensure an IES file exists on disk and return its path. Writes the file if the original path no longer exists. Args: folder: Optional folder to write IES file to. The file is only written if the path stored in the ies_path property cannot be accessed. Returns: Path of IES file. """ if self._ies_path and os.path.isfile(self._ies_path): return self._ies_path if self._ies_content is None: raise RuntimeError('IES content is missing.') folder = folder or os.getcwd() if not os.path.isdir(folder): os.makedirs(folder) path = os.path.join(folder, '{}.ies'.format(self.identifier)) with io.open(path, 'w', encoding='utf-8') as f: f.write(self._ies_content) self._ies_path = path return path def _identifier_from_ies_content(self): """Generate name of luminaire from IES content.""" if not self._ies_content: return None for line in self._ies_content.splitlines(): line = line.strip() if not line.startswith('['): continue if line.upper().startswith('[LUMINAIRE]'): if line.split(']', 1)[-1].strip(): return line.split(']', 1)[-1].strip() if line.upper().startswith('[LUMCAT]'): if line.split(']', 1)[-1].strip(): return line.split(']', 1)[-1].strip() return None
[docs] def to_dict(self): """Return Luminaire as a dictionary.""" base = { 'type': 'Luminaire', 'ies_path': self.ies_path, 'ies_content': self._ies_content, 'identifier': self.identifier, 'light_loss_factor': self.light_loss_factor, 'candela_multiplier': self.candela_multiplier } if self.luminaire_zone is not None: base['luminaire_zone'] = self.luminaire_zone.to_dict() if self.custom_lamp is not None: base['custom_lamp'] = self.custom_lamp.to_dict() return base
def __str__(self): return self.__repr__()
[docs] def ToString(self): """Overwrite ToString .NET method.""" return self.__repr__()
def __repr__(self): """Get the string representation of the the luminaire.""" n_luminaires = len(self.luminaire_zone.instances) if self.luminaire_zone else 0 return 'Luminaire: {} [LuminaireZone: {}]'.format(self.identifier, n_luminaires)
[docs] class LuminaireZone(object): """A collection of luminaire instances defining a lighting layout. A LuminaireZone groups one or more LuminaireInstance objects together to describe how a Luminaire is distributed, positioned, and oriented within a scene. The LuminaireZone is attached to a Luminaire object and is required for generating positioned Radiance geometry (xform output). Typical use cases include: - Repeated luminaires in a grid - Multiple luminaires sharing the same photometric definition - Luminaires with varying orientation and aiming directions Args: instances: A list of LuminaireInstance objects. Properties: * instances * points * spins * tilts * rotations """ __slots__ = ('_instances',) def __init__(self, instances): self.instances = instances
[docs] @classmethod def from_dict(cls, data): """Create a LuminaireZone from a dictionary. Args: data: A python dictionary of a LuminaireZone. """ instances = [LuminaireInstance.from_dict(d) for d in data['instances']] return cls(instances)
@property def instances(self): """Get a list of LuminaireInstance objects.""" return self._instances @instances.setter def instances(self, value): if not all(isinstance(v, LuminaireInstance) for v in value): raise TypeError('All items must be LuminaireInstance objects.') self._instances = list(value) @property def points(self): """Return a list of points.""" return [i.point for i in self.instances] @property def spins(self): """Return a list of spin values.""" return [i.spin for i in self.instances] @property def tilts(self): """Return a list of tilt values.""" return [i.tilt for i in self.instances] @property def rotations(self): """Return a list of rotation values.""" return [i.rotation for i in self.instances] def __len__(self): return len(self._instances) def __iter__(self): return iter(self._instances)
[docs] def to_dict(self): """Return LuminaireZone as a dictionary.""" return { 'type': 'LuminaireZone', 'instances': [i.to_dict() for i in self.instances] }
def __str__(self): return self.__repr__()
[docs] def ToString(self): """Overwrite ToString .NET method.""" return self.__repr__()
def __repr__(self): """Get the string representation of the luminaire zone.""" return 'LuminaireZone [Instances: {}] '.format(len(self))
[docs] class LuminaireInstance(object): """A single positioned and oriented instance of a luminaire. A LuminaireInstance represents one physical placement of a luminaire in space. It defines the luminaire's location and orientation using IES LM-63 C0-G0 conventions. Orientation is defined using three angular values (in degrees): A single Luminaire may have many LuminaireInstance objects, typically grouped together within a LuminaireZone. Args: point: Location of the luminaire instance. Can be a Ladybug Geometry Point3D or a list/tuple of three numeric values [x, y, z]. spin: Rotation about the local vertical axis (degrees). Default: 0. tilt: Tilt angle away from the negative Z axis (degrees). Default: 0. rotation: Rotation in the horizontal plane about the C0 axis (degrees). Default: 0. Properties: * point * spin * tilt * rotation """ __slots__ = ('_point', '_spin', '_tilt', '_rotation') def __init__(self, point, spin=0, tilt=0, rotation=0): self.point = point self.spin = spin self.tilt = tilt self.rotation = rotation
[docs] @classmethod def from_dict(cls, data): """Create a LuminaireInstance from a dictionary. Args: data: A python dictionary of a LuminaireInstance. """ point = Point3D.from_dict(data['point']) spin = data.get('spin', 0) tilt = data.get('tilt', 0) rotation = data.get('rotation', 0) return cls(point, spin=spin, tilt=tilt, rotation=rotation)
[docs] @classmethod def from_aiming_point(cls, point, aiming_point, spin=0, tilt=0, rotation=0): """Create a LuminaireInstance aimed at a target point. Args: point: Luminaire location. Can be Point3D or list/tuple of three values. aiming_point: Target point the luminaire should aim at. Can be Point3D or list/tuple of three values. spin: User-defined spin offset (degrees). tilt: User-defined tilt offset (degrees). rotation: User-defined rotation offset (degrees). """ if not isinstance(point, Point3D): point = Point3D(*point) if not isinstance(aiming_point, Point3D): aiming_point = Point3D(*aiming_point) pt_vec = aiming_point - point if pt_vec.magnitude == 0: return cls(point, spin=spin, tilt=tilt, rotation=rotation) pt_vec = pt_vec.normalize() C0_vec = Vector3D(1, 0, 0) G0_vec = Vector3D(0, 0, -1) angle_G0 = math.degrees(pt_vec.angle(G0_vec)) angle_G0 = 360 - angle_G0 proj = Vector3D(pt_vec.x, pt_vec.y, 0) if proj.magnitude == 0: angle_C0 = 0 else: proj = proj.normalize() angle_C0 = math.degrees(C0_vec.angle(proj)) if C0_vec.cross(proj).z < 0: angle_C0 = 360 - angle_C0 tilt = angle_G0 + tilt rotation = angle_C0 + rotation return cls(point, spin=spin, tilt=tilt, rotation=rotation)
@property def point(self): """Get the Point3D object of the LuminaireInstance.""" return self._point @point.setter def point(self, value): if not isinstance(value, Point3D): value = Point3D(*value) self._point = value @property def spin(self): """Get the spin value of the LuminaireInstance.""" return self._spin @spin.setter def spin(self, value): self._spin = float(value) @property def tilt(self): """Get the tilt value of the LuminaireInstance.""" return self._tilt @tilt.setter def tilt(self, value): self._tilt = float(value) @property def rotation(self): """Get the rotation value of the LuminaireInstance.""" return self._rotation @rotation.setter def rotation(self, value): self._rotation = float(value)
[docs] def to_dict(self): """Return LuminaireInstance as a dictionary.""" return { 'point': self.point.to_dict(), 'spin': self.spin, 'tilt': self.tilt, 'rotation': self.rotation }
def __str__(self): return self.__repr__()
[docs] def ToString(self): """Overwrite ToString .NET method.""" return self.__repr__()
def __repr__(self): """Get the string representation of the luminaire instance.""" return 'LuminaireInstance [Point: {}, Spin: {}, Tilt: {}, Rotation: {}] '.format( self.point, round(self.spin, 2), round(self.tilt, 2), round(self.rotation, 2))
[docs] class CustomLamp(object): """Custom lamp definition for a luminaire. A CustomLamp overrides or modifies the luminous output of an IES-defined luminaire without changing its photometric distribution. Args: name: Text identifier for the lamp. depreciation_factor: Lumen depreciation factor (0-1). rgb: Optional RGB tuple (r, g, b), values 0-1. white_xy: Optional CIE xy tuple (x, y). color_temperature: Optional color temperature in Kelvin. """ __slots__ = ( '_name', '_depreciation_factor', '_candela_multiplier', '_rgb', '_white_xy', '_color_temperature', '_metadata' ) def __init__( self, name, depreciation_factor=1.0, candela_multiplier=1.0, rgb=None, white_xy=None, color_temperature=None ): if rgb is not None and white_xy is not None: raise ValueError('CustomLamp cannot define both rgb and white_xy.') if rgb is None and white_xy is None: raise ValueError('CustomLamp must define either rgb or white_xy.') if not (0.0 <= depreciation_factor <= 1.0): raise ValueError('depreciation_factor must be between 0 and 1.') if rgb is not None: if len(rgb) != 3 or any(c < 0 or c > 1 for c in rgb): raise ValueError('RGB values must be between 0 and 1.') if white_xy is not None: if len(white_xy) != 2 or any(c < 0 or c > 1 for c in white_xy): raise ValueError('white_xy values must be between 0 and 1.') self._name = str(name) self._depreciation_factor = float(depreciation_factor) self._candela_multiplier = float(candela_multiplier) self._rgb = tuple(rgb) if rgb is not None else None self._white_xy = tuple(white_xy) if white_xy is not None else None self._color_temperature = color_temperature
[docs] @classmethod def from_lamp_name(cls, lamp_name): """Create a CustomLamp from a lamp name from a list of predefined lamps. Args: lamp_name: Name matching a legacy Honeybee lamp type. depreciation_factor: Optional lumen depreciation factor. Returns: CustomLamp """ key = lamp_name.lower().strip() if key not in LAMPNAMES: raise ValueError( 'Unknown lamp name "{}". Valid options are:\n{}'.format( lamp_name, ', '.join(sorted(LAMPNAMES.keys())) ) ) x, y, depreciation_factor = LAMPNAMES[key] return cls( name=lamp_name, white_xy=(x, y), depreciation_factor=depreciation_factor )
[docs] @classmethod def from_color_temperature(cls, name, color_temperature, depreciation_factor=1.0): """Create a CustomLamp from a correlated color temperature (CCT). Args: name: Name of the lamp. color_temperature: CCT in Kelvin (1000-25000). depreciation_factor: Optional lumen depreciation factor. Returns: CustomLamp instance with white_xy computed from CCT. """ # validate the CCT range if not (1000 <= color_temperature <= 25000): raise ValueError("The color temperature value should be between 1000 and 25000.") # convert CCT to xy using the legacy function wavelengths = {wavelength:wavelength*(10**-9) for wavelength in range(360,831)} x, y = calc_xy_1931_from_cct(color_temperature, wavelengths, CMFS) # compute color coordinates (u, v) for 1960 and 1976 standards cor = color_coordinates(x, y, 1931) uv1960 = cor[1960] uv1976 = cor[1976] # default Duv is 0 duv = 0.0 lamp_metadata = { 'uv1960': uv1960, 'uv1976': uv1976, 'CCT': color_temperature, 'Duv': duv } lamp = cls( name=name, white_xy=(x, y), color_temperature=color_temperature, depreciation_factor=depreciation_factor ) lamp._metadata = lamp_metadata return lamp
[docs] @classmethod def from_xy_coordinates(cls, name, x, y, depreciation_factor=1.0, color_space=0): """Create a CustomLamp from CIE xy coordinates. Args: name: Name of the lamp. x, y: CIE xy coordinates (0-1). depreciation_factor: Optional lumen depreciation factor. color_space: Optional color space (0=1931, 1=1960, 2=1976). Returns: CustomLamp instance with metadata. """ if color_space not in (0, 1, 2): raise ValueError( "The value for color_space should be 0, 1, or 2. " "0=1931 CIE, 1=1960 CIE, 2=1976 CIE." ) year = {0: 1931, 1: 1960, 2: 1976}[color_space] cor = color_coordinates(x, y, year) x1931, y1931 = cor[1931] u1960, v1960 = cor[1960] u1976, v1976 = cor[1976] cct, duv = calc_cct(x1931, y1931, 1931) if abs(duv) > 0.02: cct, duv = "NA", "NA" # prepare metadata lamp_metadata = { 'uv1960': (u1960, v1960), 'uv1976': (u1976, v1976), 'CCT': cct, 'Duv': duv, 'color_space': year } # create the lamp lamp = cls(name=name, white_xy=(x1931, y1931), depreciation_factor=depreciation_factor) lamp._metadata = lamp_metadata return lamp
[docs] @classmethod def from_rgb_colors(cls, name, rgb_color, depreciation_factor=1.0): """Create a CustomLamp from RGB colors. Args: name: Name of the lamp. rgb_color: Tuple/list with values (R, G, B, A) in 0-255. depreciation_factor: Optional lumen depreciation factor. Returns: CustomLamp instance with metadata. """ # extract and normalize RGB values if len(rgb_color) == 3: # set alpha to 1 rgb_color.append(1) r, g, b, a = rgb_color[0], rgb_color[1], rgb_color[2], rgb_color[3] r, g, b, a = map(lambda c: round(float(c) / 255.0, 4), (r, g, b, a)) # adjust depreciation factor with alpha effective_depr = depreciation_factor * a # prepare metadata lamp_metadata = { 'rgb': (r, g, b), 'alpha': a, 'effective_depreciation': effective_depr, 'lamp_type': 'RGB' } # Create the lamp lamp = cls(name=name, rgb=(r, g, b), depreciation_factor=effective_depr) lamp._metadata = lamp_metadata return lamp
[docs] @classmethod def from_default_white(cls, name, depreciation_factor=1.0): """Create a default white CustomLamp with 3200 K CCT. Args: name: Name of the lamp. depreciation_factor: Optional lumen depreciation factor. Returns: CustomLamp instance with metadata. """ # default CCT cct_default = 3200 # convert to xy using helper wavelengths = {wavelength: wavelength * 1e-9 for wavelength in range(360, 831)} x, y = calc_xy_1931_from_cct(cct_default, wavelengths, CMFS) cor = color_coordinates(x, y, 1931) # extract coordinates for different color spaces x1931, y1931 = cor[1931] u1960, v1960 = cor[1960] u1976, v1976 = cor[1976] # Duv is 0.0 for default duv = 0.0 # prepare metadata lamp_metadata = { 'uv1960': (u1960, v1960), 'uv1976': (u1976, v1976), 'CCT': cct_default, 'Duv': duv, 'lamp_type': 'Default White' } # Create the lamp instance lamp = cls(name=name, white_xy=(x1931, y1931), depreciation_factor=depreciation_factor) lamp._metadata = lamp_metadata return lamp
[docs] @classmethod def from_dict(cls, data): """Create a CustomLamp from a dictionary.""" return cls( name=data['name'], depreciation_factor=data.get('depreciation_factor', 1.0), candela_multiplier=data.get('candela_multiplier', 1.0), rgb=data.get('rgb'), white_xy=data.get('white_xy') )
@property def name(self): """Return lamp name.""" return self._name @property def depreciation_factor(self): """Return depreciation factor.""" return self._depreciation_factor @property def candela_multiplier(self): """Return candela multiplier.""" return self._candela_multiplier @property def is_rgb(self): """Return boolean to note whether or not lamp is RGB.""" return self._rgb is not None @property def is_white(self): """Return boolean to note whether or not lamp is White.""" return self._white_xy is not None @property def rgb(self): """Return RGB values.""" return self._rgb @property def white_xy(self): """Rwturn x, y chromaticities.""" return self._white_xy @property def color_temperature(self): """Correlated color temperature in Kelvin, if defined.""" return self._color_temperature @property def metadata(self): """Return lamp metadata.""" return getattr(self, "_metadata", None)
[docs] def to_dict(self): """Convert CustomLamp to dictionary.""" return { 'name': self._name, 'depreciation_factor': self._depreciation_factor, 'candela_multiplier': self._candela_multiplier, 'rgb': self._rgb, 'white_xy': self._white_xy }
def __str__(self): return self.__repr__()
[docs] def ToString(self): """Overwrite ToString .NET method.""" return self.__repr__()
def __repr__(self): mode = 'RGB' if self.is_rgb else 'White' return ( 'CustomLamp: {} [Type: {}]' ).format(self._name, mode)
[docs] def color_coordinates(a, b, year): """Convert CIE color coordinates between 1931, 1960, and 1976 systems. This function takes a pair of chromaticity coordinates defined in one CIE color space and returns the equivalent coordinates in all three standard CIE systems. Args: a: First chromaticity coordinate (x or u or u'). b: Second chromaticity coordinate (y or v or v'). year: CIE diagram year. Must be one of - 1931 (x, y) - 1960 (u, v) - 1976 (u', v') Returns: dict: Dictionary with keys {1931, 1960, 1976}, each mapped to a (a, b) tuple rounded to 8 decimal places. Example: { 1931: (x, y), 1960: (u, v), 1976: (u_prime, v_prime) } """ if year not in (1931, 1960, 1976): raise ValueError('Year must be one of 1931, 1960, or 1976.') coords = {} if year == 1931: x, y = float(a), float(b) u = (4 * x) / (-2 * x + 12 * y + 3) v = (6 * y) / (-2 * x + 12 * y + 3) coords[1931] = (x, y) coords[1960] = (u, v) coords[1976] = (u, v * 1.5) elif year == 1960: u, v = float(a), float(b) v_prime = v * 1.5 x = 9 * u / (6 * u - 16 * v_prime + 12) y = 4 * v_prime / (6 * u - 16 * v_prime + 12) coords[1960] = (u, v) coords[1976] = (u, v_prime) coords[1931] = (x, y) elif year == 1976: u_prime, v_prime = float(a), float(b) v = v_prime / 1.5 x = 9 * u_prime / (6 * u_prime - 16 * v_prime + 12) y = 4 * v_prime / (6 * u_prime - 16 * v_prime + 12) coords[1976] = (u_prime, v_prime) coords[1960] = (u_prime, v) coords[1931] = (x, y) for key, value in coords.items(): coords[key] = (round(value[0], 8), round(value[1], 8)) return coords
[docs] def calc_xy_1931_from_cct(cct, wavelengths, cmfs): """Calculate CIE 1931 x,y chromaticity from correlated color temperature. This function computes the chromaticity coordinates of a blackbody radiator at the given color temperature using Planck's law and the CIE 1931 color matching functions. The implementation matches the legacy Honeybee Custom Lamp behavior, including normalization at 560 nm. Args: cct: Correlated color temperature in Kelvin. Valid range is approximately 1000-25000 K. wavelengths: Mapping of wavelength (nm) > wavelength in meters. Expected keys: integers from 360 to 830. cmfs: Mapping of wavelength (nm) > (x, y, <) CIE 1931 values. Returns: tuple: (x, y) CIE 1931 chromaticity coordinates, rounded to 8 decimals. Raises: ValueError: If CCT is outside a reasonable range or data is missing. """ if cct <= 0: raise ValueError('Color temperature must be greater than zero.') # physical constants (Planck's law) c1 = 3.741771e-16 c2 = 1.4387769e-2 # normalize spectral power distribution at 560 nm wl_560 = wavelengths.get(560) if wl_560 is None: raise ValueError('Wavelengths must include 560 nm.') exp = math.exp spd_560 = ( c1 * (wl_560 ** -5) / (exp(c2 / (cct * wl_560)) - 1) ) if spd_560 == 0: raise ValueError('Invalid spectral power normalization.') # compute normalized spectral power distribution spectral_power = {} for wl in range(360, 831): wl_m = wavelengths.get(wl) if wl_m is None: raise ValueError('Missing wavelength data at {} nm'.format(wl)) spectral_power[wl] = ( c1 * (wl_m ** -5) / (exp(c2 / (cct * wl_m)) - 1) ) / spd_560 # tristimulus integration tri_x = tri_y = tri_z = 0.0 for wl in range(360, 831): xbar, ybar, zbar = cmfs[wl] spd = spectral_power[wl] tri_x += 683 * xbar * spd tri_y += 683 * ybar * spd tri_z += 683 * zbar * spd denom = tri_x + tri_y + tri_z if denom == 0: raise ValueError('Invalid tristimulus calculation.') x = tri_x / denom y = tri_y / denom return round(x, 8), round(y, 8)
[docs] def planckian_table(u_src, v_src, min_temp=1000, max_temp=100000, growth=1.01): """Build a table of Planckian temperatures along with (u, v) coordinates and distances. Args: u_src, v_src: Reference u,v coordinates (CIE 1960) to compare against. min_temp: Starting temperature (K). Default 1000 K. max_temp: Maximum temperature (K). Default 100,000 K. growth: Multiplicative growth factor per step. Default 1.01 (~1% step). Returns: List of tuples: (temperature, u, v, distance_to_ref, counter) """ table = [] temp = min_temp counter = 1 wavelengths = {wavelength:wavelength*(10**-9) for wavelength in range(360,831)} while temp < max_temp: temp *= growth # compute 1931 xy from temp x, y = calc_xy_1931_from_cct(temp, wavelengths, CMFS) # convert to 1960 uv u, v = color_coordinates(x, y, 1931)[1960] # distance to reference point dist = math.sqrt((u_src - u) ** 2 + (v_src - v) ** 2) table.append((temp, u, v, dist, counter)) counter += 1 return table
[docs] def calc_cct(a, b, year): """Calculate Correlated Color Temperature (CCT) and Duv from CIE coordinates. Args: a, b: CIE coordinates (x,y or u,v or u',v') depending on `year`. year: Year of the coordinates (1931, 1960, 1976) Returns: Tuple: (CCT in K, Duv) """ # convert input coordinates to CIE 1960 uv u, v = color_coordinates(a, b, year)[1960] # build Planckian table table = planckian_table(u, v) # distance extraction distances = [row[3] for row in table] min_dist = min(distances) min_idx = distances.index(min_dist) # triangular sign helper sign = lambda x: -1 if x < 0 else 1 try: pt_minus1 = table[min_idx - 1] pt = table[min_idx] pt_plus1 = table[min_idx + 1] except IndexError: # edge case: CCT too high or too low return 10000, 0.1 # extract distances and temperatures dm1, dm, dp1 = pt_minus1[3], pt[3], pt_plus1[3] tm1, tm, tp1 = pt_minus1[0], pt[0], pt_plus1[0] um1, vm1 = pt_minus1[1:3] up1, vp1 = pt_plus1[1:3] # distance along uv line l = math.sqrt((um1 - up1) ** 2 + (vm1 - vp1) ** 2) x = (dm1**2 - dp1**2 + l**2) / 2 vtx = vm1 + (vp1 - vm1) * x / l sign_val = sign(v - vtx) # triangular interpolation tx_tri = tm1 + (tp1 - tm1) * (x / l) duv_tri = math.sqrt(dm1**2 - x**2) * sign_val # parabolic interpolation X = (tp1 - tm) * (tm1 - tp1) * (tm - tm1) a_coef = (tm1*(dp1 - dm) + tm*(dm1 - dp1) + tp1*(dm - dm1)) / X b_coef = -((tm1**2)*(dp1 - dm) + (tm**2)*(dm1 - dp1) + (tp1**2)*(dm - dm1)) / X c_coef = -(dm1*(tp1 - tm)*tm*tp1 + dm*(tm1 - tp1)*tm1*tp1 + dp1*(tm - tm1)*tm1*tm) / X tx = -b_coef / (2 * a_coef) tx_cor = tx * 0.99991 duv = sign_val * (a_coef*tx_cor**2 + b_coef*tx_cor + c_coef) # return triangular if Duv very small if abs(duv) < 0.002: return tx_tri, duv_tri else: return tx, duv
LAMPNAMES = { 'clear metal halide': (0.396, 0.39, 0.8), 'cool white deluxe': (0.376, 0.368, 0.85), 'deluxe cool white': (0.376, 0.368, 0.85), 'deluxe warm white': (0.440, 0.403, 0.85), 'fluorescent': (0.376, 0.368, 0.85), 'incandescent': (0.453,0.405, 0.95), 'mercury': (0.373, 0.415, 0.8), 'metal halide': (0.396, 0.390, 0.8), 'halogen': (0.4234, 0.399, 1), 'quartz': (0.424, 0.399, 1), 'sodium':(0.569, 0.421, 0.93), 'warm white deluxe': (0.440, 0.403, 0.85), 'xenon': (0.324, 0.324,1), 'warm white': (0.440, 0.403, 0.85), 'cool white': (0.376, 0.368, 0.85) } CMFS = { 360: (0.000130, 0.000004, 0.000606), 361: (0.000146, 0.000004, 0.000681), 362: (0.000164, 0.000005, 0.000765), 363: (0.000184, 0.000006, 0.000860), 364: (0.000207, 0.000006, 0.000967), 365: (0.000232, 0.000007, 0.001086), 366: (0.000261, 0.000008, 0.001221), 367: (0.000293, 0.000009, 0.001373), 368: (0.000329, 0.000010, 0.001544), 369: (0.000370, 0.000011, 0.001734), 370: (0.000415, 0.000012, 0.001946), 371: (0.000464, 0.000014, 0.002178), 372: (0.000519, 0.000016, 0.002436), 373: (0.000582, 0.000017, 0.002732), 374: (0.000655, 0.000020, 0.003078), 375: (0.000742, 0.000022, 0.003486), 376: (0.000845, 0.000025, 0.003975), 377: (0.000965, 0.000028, 0.004541), 378: (0.001095, 0.000032, 0.005158), 379: (0.001231, 0.000035, 0.005803), 380: (0.001368, 0.000039, 0.006450), 381: (0.001502, 0.000043, 0.007083), 382: (0.001642, 0.000047, 0.007745), 383: (0.001802, 0.000052, 0.008501), 384: (0.001996, 0.000057, 0.009415), 385: (0.002236, 0.000064, 0.010550), 386: (0.002535, 0.000072, 0.011966), 387: (0.002893, 0.000082, 0.013656), 388: (0.003301, 0.000094, 0.015588), 389: (0.003753, 0.000106, 0.017730), 390: (0.004243, 0.000120, 0.020050), 391: (0.004762, 0.000135, 0.022511), 392: (0.005330, 0.000151, 0.025203), 393: (0.005979, 0.000170, 0.028280), 394: (0.006741, 0.000192, 0.031897), 395: (0.007650, 0.000217, 0.036210), 396: (0.008751, 0.000247, 0.041438), 397: (0.010029, 0.000281, 0.047504), 398: (0.011422, 0.000319, 0.054120), 399: (0.012869, 0.000357, 0.060998), 400: (0.014310, 0.000396, 0.067850), 401: (0.015704, 0.000434, 0.074486), 402: (0.017147, 0.000473, 0.081362), 403: (0.018781, 0.000518, 0.089154), 404: (0.020748, 0.000572, 0.098540), 405: (0.023190, 0.000640, 0.110200), 406: (0.026207, 0.000725, 0.124613), 407: (0.029782, 0.000826, 0.141702), 408: (0.033881, 0.000941, 0.161304), 409: (0.038468, 0.001070, 0.183257), 410: (0.043510, 0.001210, 0.207400), 411: (0.048996, 0.001362, 0.233692), 412: (0.055023, 0.001531, 0.262611), 413: (0.061719, 0.001720, 0.294775), 414: (0.069212, 0.001935, 0.330799), 415: (0.077630, 0.002180, 0.371300), 416: (0.086958, 0.002455, 0.416209), 417: (0.097177, 0.002764, 0.465464), 418: (0.108406, 0.003118, 0.519695), 419: (0.120767, 0.003526, 0.579530), 420: (0.134380, 0.004000, 0.645600), 421: (0.149358, 0.004546, 0.718484), 422: (0.165396, 0.005159, 0.796713), 423: (0.181983, 0.005829, 0.877846), 424: (0.198611, 0.006546, 0.959439), 425: (0.214770, 0.007300, 1.039050), 426: (0.230187, 0.008087, 1.115367), 427: (0.244880, 0.008909, 1.188497), 428: (0.258777, 0.009768, 1.258123), 429: (0.271808, 0.010664, 1.323930), 430: (0.283900, 0.011600, 1.385600), 431: (0.294944, 0.012573, 1.442635), 432: (0.304897, 0.013583, 1.494804), 433: (0.313787, 0.014630, 1.542190), 434: (0.321645, 0.015715, 1.584881), 435: (0.328500, 0.016840, 1.622960), 436: (0.334351, 0.018007, 1.656405), 437: (0.339210, 0.019214, 1.685296), 438: (0.343121, 0.020454, 1.709875), 439: (0.346130, 0.021718, 1.730382), 440: (0.348280, 0.023000, 1.747060), 441: (0.349600, 0.024295, 1.760045), 442: (0.350147, 0.025610, 1.769623), 443: (0.350013, 0.026959, 1.776264), 444: (0.349287, 0.028351, 1.780433), 445: (0.348060, 0.029800, 1.782600), 446: (0.346373, 0.031311, 1.782968), 447: (0.344262, 0.032884, 1.781700), 448: (0.341809, 0.034521, 1.779198), 449: (0.339094, 0.036226, 1.775867), 450: (0.336200, 0.038000, 1.772110), 451: (0.333198, 0.039847, 1.768259), 452: (0.330041, 0.041768, 1.764039), 453: (0.326636, 0.043766, 1.758944), 454: (0.322887, 0.045843, 1.752466), 455: (0.318700, 0.048000, 1.744100), 456: (0.314025, 0.050244, 1.733560), 457: (0.308884, 0.052573, 1.720858), 458: (0.303290, 0.054981, 1.705937), 459: (0.297258, 0.057459, 1.688737), 460: (0.290800, 0.060000, 1.669200), 461: (0.283970, 0.062602, 1.647529), 462: (0.276721, 0.065278, 1.623413), 463: (0.268918, 0.068042, 1.596022), 464: (0.260423, 0.070911, 1.564528), 465: (0.251100, 0.073900, 1.528100), 466: (0.240848, 0.077016, 1.486111), 467: (0.229851, 0.080266, 1.439522), 468: (0.218407, 0.083667, 1.389880), 469: (0.206812, 0.087233, 1.338736), 470: (0.195360, 0.090980, 1.287640), 471: (0.184214, 0.094918, 1.237422), 472: (0.173327, 0.099046, 1.187824), 473: (0.162688, 0.103367, 1.138761), 474: (0.152283, 0.107885, 1.090148), 475: (0.142100, 0.112600, 1.041900), 476: (0.132179, 0.117532, 0.994198), 477: (0.122570, 0.122674, 0.947347), 478: (0.113275, 0.127993, 0.901453), 479: (0.104298, 0.133453, 0.856619), 480: (0.095640, 0.139020, 0.812950), 481: (0.087300, 0.144676, 0.770517), 482: (0.079308, 0.150469, 0.729445), 483: (0.071718, 0.156462, 0.689914), 484: (0.064581, 0.162718, 0.652105), 485: (0.057950, 0.169300, 0.616200), 486: (0.051862, 0.176243, 0.582329), 487: (0.046282, 0.183558, 0.550416), 488: (0.041151, 0.191274, 0.520338), 489: (0.036413, 0.199418, 0.491967), 490: (0.032010, 0.208020, 0.465180), 491: (0.027917, 0.217120, 0.439925), 492: (0.024144, 0.226735, 0.416184), 493: (0.020687, 0.236857, 0.393882), 494: (0.017540, 0.247481, 0.372946), 495: (0.014700, 0.258600, 0.353300), 496: (0.012162, 0.270185, 0.334858), 497: (0.009920, 0.282294, 0.317552), 498: (0.007967, 0.295051, 0.301338), 499: (0.006296, 0.308578, 0.286169), 500: (0.004900, 0.323000, 0.272000), 501: (0.003777, 0.338402, 0.258817), 502: (0.002945, 0.354686, 0.246484), 503: (0.002425, 0.371699, 0.234772), 504: (0.002236, 0.389288, 0.223453), 505: (0.002400, 0.407300, 0.212300), 506: (0.002926, 0.425630, 0.201169), 507: (0.003837, 0.444310, 0.190120), 508: (0.005175, 0.463394, 0.179225), 509: (0.006982, 0.482940, 0.168561), 510: (0.009300, 0.503000, 0.158200), 511: (0.012149, 0.523569, 0.148138), 512: (0.015536, 0.544512, 0.138376), 513: (0.019478, 0.565690, 0.128994), 514: (0.023993, 0.586965, 0.120075), 515: (0.029100, 0.608200, 0.111700), 516: (0.034815, 0.629346, 0.103905), 517: (0.041120, 0.650307, 0.096667), 518: (0.047985, 0.670875, 0.089983), 519: (0.055379, 0.690842, 0.083845), 520: (0.063270, 0.710000, 0.078250), 521: (0.071635, 0.728185, 0.073209), 522: (0.080462, 0.745464, 0.068678), 523: (0.089740, 0.761969, 0.064568), 524: (0.099456, 0.777837, 0.060788), 525: (0.109600, 0.793200, 0.057250), 526: (0.120167, 0.808110, 0.053904), 527: (0.131115, 0.822496, 0.050747), 528: (0.142368, 0.836307, 0.047753), 529: (0.153854, 0.849492, 0.044899), 530: (0.165500, 0.862000, 0.042160), 531: (0.177257, 0.873811, 0.039507), 532: (0.189140, 0.884962, 0.036936), 533: (0.201169, 0.895494, 0.034458), 534: (0.213366, 0.905443, 0.032089), 535: (0.225750, 0.914850, 0.029840), 536: (0.238321, 0.923735, 0.027712), 537: (0.251067, 0.932092, 0.025694), 538: (0.263992, 0.939923, 0.023787), 539: (0.277102, 0.947225, 0.021989), 540: (0.290400, 0.954000, 0.020300), 541: (0.303891, 0.960256, 0.018718), 542: (0.317573, 0.966007, 0.017240), 543: (0.331438, 0.971261, 0.015864), 544: (0.345483, 0.976023, 0.014585), 545: (0.359700, 0.980300, 0.013400), 546: (0.374084, 0.984092, 0.012307), 547: (0.388640, 0.987481, 0.011302), 548: (0.403378, 0.990313, 0.010378), 549: (0.418312, 0.992812, 0.009529), 550: (0.433450, 0.994950, 0.008750), 551: (0.448795, 0.996711, 0.008035), 552: (0.464336, 0.998098, 0.007382), 553: (0.480064, 0.999112, 0.006785), 554: (0.495971, 0.999748, 0.006243), 555: (0.512050, 1.000000, 0.005750), 556: (0.528296, 0.999857, 0.005304), 557: (0.544692, 0.999305, 0.004900), 558: (0.561209, 0.998326, 0.004534), 559: (0.577822, 0.996899, 0.004202), 560: (0.594500, 0.995000, 0.003900), 561: (0.611221, 0.992601, 0.003623), 562: (0.627976, 0.989743, 0.003371), 563: (0.644760, 0.986444, 0.003141), 564: (0.661570, 0.982724, 0.002935), 565: (0.678400, 0.978600, 0.002750), 566: (0.695239, 0.974084, 0.002585), 567: (0.712059, 0.969171, 0.002439), 568: (0.728828, 0.963857, 0.002309), 569: (0.745519, 0.958135, 0.002197), 570: (0.762100, 0.952000, 0.002100), 571: (0.778543, 0.945450, 0.002018), 572: (0.794826, 0.938499, 0.001948), 573: (0.810926, 0.931163, 0.001890), 574: (0.826825, 0.923458, 0.001841), 575: (0.842500, 0.915400, 0.001800), 576: (0.857933, 0.907006, 0.001766), 577: (0.873082, 0.898277, 0.001738), 578: (0.887894, 0.889205, 0.001711), 579: (0.902318, 0.879782, 0.001683), 580: (0.916300, 0.870000, 0.001650), 581: (0.929800, 0.859861, 0.001610), 582: (0.942798, 0.849392, 0.001564), 583: (0.955278, 0.838622, 0.001514), 584: (0.967218, 0.827581, 0.001459), 585: (0.978600, 0.816300, 0.001400), 586: (0.989386, 0.804795, 0.001337), 587: (0.999549, 0.793082, 0.001270), 588: (1.009089, 0.781192, 0.001205), 589: (1.018006, 0.769155, 0.001147), 590: (1.026300, 0.757000, 0.001100), 591: (1.033983, 0.744754, 0.001069), 592: (1.049860, 0.732422, 0.001049), 593: (1.047188, 0.720004, 0.001036), 594: (1.052467, 0.707497, 0.001021), 595: (1.056700, 0.694900, 0.001000), 596: (1.059794, 0.682219, 0.000969), 597: (1.061799, 0.669472, 0.000930), 598: (1.062807, 0.656674, 0.000887), 599: (1.062910, 0.643845, 0.000843), 600: (1.062200, 0.631000, 0.000800), 601: (1.067352, 0.618156, 0.000761), 602: (1.058444, 0.605314, 0.000724), 603: (1.055224, 0.592476, 0.000686), 604: (1.050977, 0.579638, 0.000645), 605: (1.045600, 0.566800, 0.000600), 606: (1.039037, 0.553961, 0.000548), 607: (1.031361, 0.541137, 0.000492), 608: (1.022666, 0.528353, 0.000435), 609: (1.013048, 0.515632, 0.000383), 610: (1.002600, 0.503000, 0.000340), 611: (0.991368, 0.490469, 0.000307), 612: (0.979331, 0.478030, 0.000283), 613: (0.966492, 0.465678, 0.000265), 614: (0.952848, 0.453403, 0.000252), 615: (0.938400, 0.441200, 0.000240), 616: (0.923194, 0.429080, 0.000230), 617: (0.907244, 0.417036, 0.000221), 618: (0.890502, 0.405032, 0.000212), 619: (0.872920, 0.393032, 0.000202), 620: (0.854450, 0.381000, 0.000190), 621: (0.835084, 0.368918, 0.000174), 622: (0.814946, 0.356827, 0.000156), 623: (0.794186, 0.344777, 0.000136), 624: (0.772954, 0.332818, 0.000117), 625: (0.751400, 0.321000, 0.000100), 626: (0.729584, 0.309338, 0.000086), 627: (0.707589, 0.297850, 0.000075), 628: (0.685602, 0.286594, 0.000065), 629: (0.663810, 0.275625, 0.000057), 630: (0.642400, 0.265000, 0.000050), 631: (0.621515, 0.254763, 0.000044), 632: (0.601114, 0.244890, 0.000039), 633: (0.581105, 0.235334, 0.000036), 634: (0.561398, 0.226053, 0.000033), 635: (0.541900, 0.217000, 0.000030), 636: (0.522600, 0.208162, 0.000028), 637: (0.503546, 0.199549, 0.000026), 638: (0.484744, 0.191155, 0.000024), 639: (0.466194, 0.182974, 0.000022), 640: (0.447900, 0.175000, 0.000020), 641: (0.429861, 0.167224, 0.000018), 642: (0.412098, 0.159646, 0.000016), 643: (0.394644, 0.152278, 0.000014), 644: (0.377533, 0.145126, 0.000012), 645: (0.360800, 0.138200, 0.000010), 646: (0.344456, 0.131500, 0.000008), 647: (0.328517, 0.125025, 0.000005), 648: (0.313019, 0.118779, 0.000003), 649: (0.298001, 0.112769, 0.000001), 650: (0.283500, 0.107000, 0.000000), 651: (0.269545, 0.101476, 0.000000), 652: (0.256118, 0.096189, 0.000000), 653: (0.243190, 0.091123, 0.000000), 654: (0.230727, 0.086265, 0.000000), 655: (0.218700, 0.081600, 0.000000), 656: (0.207097, 0.077121, 0.000000), 657: (0.195923, 0.072826, 0.000000), 658: (0.185171, 0.068710, 0.000000), 659: (0.174832, 0.064770, 0.000000), 660: (0.164900, 0.061000, 0.000000), 661: (0.155367, 0.057396, 0.000000), 662: (0.146230, 0.053955, 0.000000), 663: (0.137490, 0.050674, 0.000000), 664: (0.129147, 0.047550, 0.000000), 665: (0.121200, 0.044580, 0.000000), 666: (0.113640, 0.041759, 0.000000), 667: (0.106465, 0.039085, 0.000000), 668: (0.099690, 0.036564, 0.000000), 669: (0.093331, 0.034200, 0.000000), 670: (0.087400, 0.032000, 0.000000), 671: (0.081901, 0.029963, 0.000000), 672: (0.076804, 0.028077, 0.000000), 673: (0.072077, 0.026329, 0.000000), 674: (0.067687, 0.024708, 0.000000), 675: (0.063600, 0.023200, 0.000000), 676: (0.059807, 0.021801, 0.000000), 677: (0.056282, 0.020501, 0.000000), 678: (0.052971, 0.019281, 0.000000), 679: (0.049819, 0.018121, 0.000000), 680: (0.046770, 0.017000, 0.000000), 681: (0.043784, 0.015904, 0.000000), 682: (0.040875, 0.014837, 0.000000), 683: (0.038073, 0.013811, 0.000000), 684: (0.035405, 0.012835, 0.000000), 685: (0.032900, 0.011920, 0.000000), 686: (0.030564, 0.011068, 0.000000), 687: (0.028381, 0.010273, 0.000000), 688: (0.026345, 0.009533, 0.000000), 689: (0.024453, 0.008846, 0.000000), 690: (0.022700, 0.008210, 0.000000), 691: (0.021084, 0.007624, 0.000000), 692: (0.019600, 0.007085, 0.000000), 693: (0.018237, 0.006591, 0.000000), 694: (0.016987, 0.006138, 0.000000), 695: (0.015840, 0.005723, 0.000000), 696: (0.014791, 0.005343, 0.000000), 697: (0.013831, 0.004996, 0.000000), 698: (0.012949, 0.004676, 0.000000), 699: (0.012129, 0.004380, 0.000000), 700: (0.011359, 0.004102, 0.000000), 701: (0.010629, 0.003838, 0.000000), 702: (0.009939, 0.003589, 0.000000), 703: (0.009288, 0.003354, 0.000000), 704: (0.008679, 0.003134, 0.000000), 705: (0.008111, 0.002929, 0.000000), 706: (0.007582, 0.002738, 0.000000), 707: (0.007089, 0.002560, 0.000000), 708: (0.006627, 0.002393, 0.000000), 709: (0.006195, 0.002237, 0.000000), 710: (0.005790, 0.002091, 0.000000), 711: (0.005410, 0.001954, 0.000000), 712: (0.005053, 0.001825, 0.000000), 713: (0.004718, 0.001704, 0.000000), 714: (0.004404, 0.001590, 0.000000), 715: (0.004109, 0.001484, 0.000000), 716: (0.003834, 0.001384, 0.000000), 717: (0.003576, 0.001291, 0.000000), 718: (0.003334, 0.001204, 0.000000), 719: (0.003109, 0.001123, 0.000000), 720: (0.002899, 0.001047, 0.000000), 721: (0.002704, 0.000977, 0.000000), 722: (0.002523, 0.000911, 0.000000), 723: (0.002354, 0.000850, 0.000000), 724: (0.002197, 0.000793, 0.000000), 725: (0.002049, 0.000740, 0.000000), 726: (0.001911, 0.000690, 0.000000), 727: (0.001781, 0.000643, 0.000000), 728: (0.001660, 0.000599, 0.000000), 729: (0.001546, 0.000558, 0.000000), 730: (0.001440, 0.000520, 0.000000), 731: (0.001340, 0.000484, 0.000000), 732: (0.001246, 0.000450, 0.000000), 733: (0.001158, 0.000418, 0.000000), 734: (0.001076, 0.000389, 0.000000), 735: (0.001000, 0.000361, 0.000000), 736: (0.000929, 0.000335, 0.000000), 737: (0.000862, 0.000311, 0.000000), 738: (0.000801, 0.000289, 0.000000), 739: (0.000743, 0.000268, 0.000000), 740: (0.000690, 0.000249, 0.000000), 741: (0.000641, 0.000231, 0.000000), 742: (0.000595, 0.000215, 0.000000), 743: (0.000552, 0.000199, 0.000000), 744: (0.000512, 0.000185, 0.000000), 745: (0.000476, 0.000172, 0.000000), 746: (0.000442, 0.000160, 0.000000), 747: (0.000412, 0.000149, 0.000000), 748: (0.000383, 0.000138, 0.000000), 749: (0.000357, 0.000129, 0.000000), 750: (0.000332, 0.000120, 0.000000), 751: (0.000310, 0.000112, 0.000000), 752: (0.000289, 0.000104, 0.000000), 753: (0.000270, 0.000097, 0.000000), 754: (0.000252, 0.000091, 0.000000), 755: (0.000235, 0.000085, 0.000000), 756: (0.000219, 0.000079, 0.000000), 757: (0.000205, 0.000074, 0.000000), 758: (0.000191, 0.000069, 0.000000), 759: (0.000178, 0.000064, 0.000000), 760: (0.000166, 0.000060, 0.000000), 761: (0.000155, 0.000056, 0.000000), 762: (0.000145, 0.000052, 0.000000), 763: (0.000135, 0.000049, 0.000000), 764: (0.000126, 0.000045, 0.000000), 765: (0.000117, 0.000042, 0.000000), 766: (0.000110, 0.000040, 0.000000), 767: (0.000102, 0.000037, 0.000000), 768: (0.000095, 0.000034, 0.000000), 769: (0.000089, 0.000032, 0.000000), 770: (0.000083, 0.000030, 0.000000), 771: (0.000078, 0.000028, 0.000000), 772: (0.000072, 0.000026, 0.000000), 773: (0.000067, 0.000024, 0.000000), 774: (0.000063, 0.000023, 0.000000), 775: (0.000059, 0.000021, 0.000000), 776: (0.000055, 0.000020, 0.000000), 777: (0.000051, 0.000018, 0.000000), 778: (0.000048, 0.000017, 0.000000), 779: (0.000044, 0.000016, 0.000000), 780: (0.000042, 0.000015, 0.000000), 781: (0.000039, 0.000014, 0.000000), 782: (0.000036, 0.000013, 0.000000), 783: (0.000034, 0.000012, 0.000000), 784: (0.000031, 0.000011, 0.000000), 785: (0.000029, 0.000011, 0.000000), 786: (0.000027, 0.000010, 0.000000), 787: (0.000026, 0.000009, 0.000000), 788: (0.000024, 0.000009, 0.000000), 789: (0.000022, 0.000008, 0.000000), 790: (0.000021, 0.000007, 0.000000), 791: (0.000019, 0.000007, 0.000000), 792: (0.000018, 0.000006, 0.000000), 793: (0.000017, 0.000006, 0.000000), 794: (0.000016, 0.000006, 0.000000), 795: (0.000015, 0.000005, 0.000000), 796: (0.000014, 0.000005, 0.000000), 797: (0.000013, 0.000005, 0.000000), 798: (0.000012, 0.000004, 0.000000), 799: (0.000011, 0.000004, 0.000000), 800: (0.000010, 0.000004, 0.000000), 801: (0.000010, 0.000003, 0.000000), 802: (0.000009, 0.000003, 0.000000), 803: (0.000008, 0.000003, 0.000000), 804: (0.000008, 0.000003, 0.000000), 805: (0.000007, 0.000003, 0.000000), 806: (0.000007, 0.000002, 0.000000), 807: (0.000006, 0.000002, 0.000000), 808: (0.000006, 0.000002, 0.000000), 809: (0.000005, 0.000002, 0.000000), 810: (0.000005, 0.000002, 0.000000), 811: (0.000005, 0.000002, 0.000000), 812: (0.000004, 0.000002, 0.000000), 813: (0.000004, 0.000001, 0.000000), 814: (0.000004, 0.000001, 0.000000), 815: (0.000004, 0.000001, 0.000000), 816: (0.000003, 0.000001, 0.000000), 817: (0.000003, 0.000001, 0.000000), 818: (0.000003, 0.000001, 0.000000), 819: (0.000003, 0.000001, 0.000000), 820: (0.000003, 0.000001, 0.000000), 821: (0.000002, 0.000001, 0.000000), 822: (0.000002, 0.000001, 0.000000), 823: (0.000002, 0.000001, 0.000000), 824: (0.000002, 0.000001, 0.000000), 825: (0.000002, 0.000001, 0.000000), 826: (0.000002, 0.000001, 0.000000), 827: (0.000002, 0.000001, 0.000000), 828: (0.000001, 0.000001, 0.000000), 829: (0.000001, 0.000000, 0.000000), 830: (0.000001, 0.000000, 0.000000) }