Hey @chris ,
I didn’t even think of chaining the parameters by piping them together in the order I wanted, but I bet that would probably work. I wound up overriding the to_radiance() function in the Xform options class to construct it using the rotation’s first just so that I knew it would be consistent. However, I do recall having a similar problem when trying to create climate based skies where sometimes the ordering of parameters would be inconsistent with what the Gendaylit command expected.
So my general use here is that I have a set of electric lights that I’m first producing custom IES profiles that are specific to a scene/activity. The particular scene I’m running into this has 24 lights, all with unique profiles. For a typical luminance render I’d have a process that looks something like this:
- Process IES files (ies2rad and xform) based on a specific scene
- Generate the Octree (oconv) using the set of files from Step 1 added to a static model
- Render the Image (rpict)
Because of how the fixtures are changing for different scenes/activities, I have a text file in my project directory named ‘luminaires.txt’ that is a CSV containing a list of IDs and the transforms for the lights that are already in a radiance format. Something like this:
The processing of the IES files, which reads the luminaire text file and runs Ies2rad and Xform based on happens in the following method. The transform is passed as read from the CSV file to the Xform.options using the update_from_string method.
@staticmethod
def process_ies(projPath, scene, color, subtype, multiplier=1.0):
"""
This will process IES luminaires for a radiance simulation
and return a list of the processed and transformed Rad files.
:param projPath: Root path for the radiance project
:param scene: Name of the scene, prefix of IES profiles to select.
:param color: Tuple of the (R,G,B) for the light color temperature.
:param subtype: LUM, SPOT, or LGP
:return: [ies file paths]
"""
# Get the Radiance Environment
env = sculpt.get_env()
# Iterate through the luminaires.txt file and set up unsculpted ies2rads
lumfile = os.path.join(projPath, "luminaires.txt")
lumFiles = []
if color is None:
color = (1.0, 0.808, 0.651)
with open(lumfile) as csvfile:
reader = csv.reader(csvfile)
for row in reader:
idx = row[0]
t = row[1]
iespath = os.path.join(projPath, 'ies', 'sculpted')
initpath = os.path.join(projPath, 'ies', 'temp') + '\\'
if scene == "unknown":
iespath = os.path.join(iespath, "unsculpted.ies")
initpath += f"_lum{idx}"
else:
iespath = os.path.join(iespath, f'{scene}_{subtype}_{idx}.ies')
initpath += pathlib.Path(iespath).stem
ies2rad = Ies2rad(None, initpath, iespath) # type: Ies2rad
ies2rad.options.c = color
ies2rad.options.m = multiplier
ies2rad.run(env, cwd=os.path.dirname(iespath))
xformPath = os.path.join(projPath, 'ies', 'temp', f"lum_{idx}.rad")
xform = Xform(None, xformPath, initpath + '.rad')
xform.options.update_from_string(t)
# run the xform command
xform.run(env, cwd=os.path.dirname(iespath))
lumFiles.append(str(xformPath))
return lumFiles
And the Xform Command and Options this uses…
"""xform Command."""
import os.path
from .options.xform import XformOptions
from ._command import Command
import warnings
import honeybee_radiance_command._typing as typing
class Xform(Command):
"""Xform command.
Xform command performs transforms on geometry...
Args:
options: Xform command options. It will be set to a null transform
if unspecified.
output: Output file (Default: None).
input: Radiance file to transform (Default: None)
Properties:
* options
* output
* input
"""
__slots__ = ('_input',)
def __init__(self, options=None, output=None, input=None):
"""Initialize Command."""
Command.__init__(self, output=output)
self._input = typing.normpath(input)
self._options = options or XformOptions()
@property
def options(self):
"""xform options."""
return self._options
@options.setter
def options(self, value):
if value is None:
print('options value is null')
value = XformOptions()
if not isinstance(value, XformOptions):
raise ValueError('Expected XformOptions not {}'.format(type(value)))
self._options = value
@property
def input(self):
"""Input file.
Get and set inputs files.
"""
return self._input
@input.setter
def input(self, value):
# ensure input is a valid file path
if not os.path.exists(value):
raise ValueError(
'the input file must be a valid, existing radiance file'
)
self._input = typing.path_checker(value)
def to_radiance(self, stdin_input=False):
"""Xform in Radiance format.
Args:
stdin_input: A boolean that indicates if the input for this command
comes from stdin. This is for instance the case when you pipe the input
from another command (default: False).
"""
self.validate()
command_parts = [self.command]
if self.options:
command_parts.append(self.options.to_radiance())
command_parts.append(self.input)
cmd = ' '.join(command_parts)
if self.pipe_to:
cmd = ' | '.join((cmd, self.pipe_to.to_radiance(stdin_input=True)))
elif self.output:
cmd = ' > '.join((cmd, self.output))
return ' '.join(cmd.split())
def validate(self):
Command.validate(self)
if not os.path.exists(self._input):
warnings.warn('xform: no valid input file provided.')
"""Xform parameters."""
from .optionbase import OptionCollection, NumericOption, IntegerOption,\
BoolOption, TupleOption
class XformOptions(OptionCollection):
"""
ies2rad -m 1 -o %~dp0ies\temp\luminaire_%%a -c %cct% -dm %~dp0ies\sculpted\%scene%_LUM_%%a.ies
[-m muliplier][-dunits][-l libdir][-p prefdir][-t lamp][-c red green blue][-f lampdat][-u lamp]
Also see: https://floyd.lbl.gov/radiance/man_html/oconv.1.html
"""
__slots__ = ('_t', '_rx', '_ry', '_rz', '_s', '_mx', '_my', '_mz', '_i')
def __init__(self):
"""Ies2rad command options."""
OptionCollection.__init__(self)
self._t = TupleOption('t', 'translate the scene along vector x y z', None, 3, float)
self._rx = NumericOption('rx', 'rotate the scene about the x axis (degrees)', None)
self._ry = NumericOption('ry', 'rotate the scene about the y axis (degrees)', None)
self._rz = NumericOption('rz', 'rotate the scene about the z axis (degrees)', None)
self._s = NumericOption('s', 'scale factor', value=1.0)
self._mx = BoolOption('mx', 'mirror about the yz plane', None)
self._my = BoolOption('my', 'mirror about the xz plane', None)
self._mz = BoolOption('mz', 'mirror about the xy plane', None)
self._i = IntegerOption('i', 'repeat the following transformations n times.', None)
self._on_setattr_check = True
def _on_setattr(self):
"""This method executes after setting each new attribute.
Use this method to add checks that are necessary for OptionCollection. For
instance in rtrace option collection -ti and -te are exclusive. You can include a
check to ensure this is always correct.
"""
# check if we need this.
# assert not (self.b.is_set and self.i.is_set), \
# 'The -b and -i options are mutually exclusive.'
@property
def t(self):
"""specify the translation vector"""
return self._t
@t.setter
def t(self, value):
self._t.value = value
@property
def rx(self):
"""specify the x axis rotation in degrees"""
return self._rx
@rx.setter
def rx(self, value):
self._rx.value = value
@property
def ry(self):
"""specify the y axis rotation in degrees"""
return self._ry
@ry.setter
def ry(self, value):
self._ry.value = value
@property
def rz(self):
"""specify the z axis rotation in degrees"""
return self._rz
@rz.setter
def rz(self, value):
self._rz.value = value
@property
def s(self):
"""Specify the scale (1.0 is 100%)"""
return self._s
@s.setter
def s(self, value):
self._s.value = value
@property
def mx(self):
"""mirror across X axis"""
return self._mx
@mx.setter
def mx(self, value):
self._mx.value = value
@property
def my(self):
"""Mirror across Y axis"""
return self._my
@my.setter
def my(self, value):
self._my.value = value
@property
def mz(self):
"""Mirror across Z axis"""
return self._mz
@mz.setter
def mz(self, value):
self._mz.value = value
@property
def i(self):
"""specify iteration quantity"""
return self._i
@i.setter
def i(self, value):
self._i.value = value
# force the parameter order
def to_radiance(self):
opt = f"-s {self._s.value}"
if self._rx != None:
opt += f" -rx {self._rx.value}"
if self._ry != None:
opt += f" -ry {self._ry.value}"
if self._rz != None:
opt += f" -rz {self._rz.value}"
if self._mx != None:
opt += f" -mx {self._mx.value}"
if self._my != None:
opt += f" -my {self._my.value}"
if self._mz != None:
opt += f" -mz {self._mz.value}"
if self._t != None:
opt += f" {self._t.to_radiance()}"
if self._i != None:
opt += f" -i {self._i.value}"
return opt
I’ll take a stab at splitting out the transforms and piping them together and report back how that turns out.
-Tim