Skip to content

Commit af72e74

Browse files
committed
[v0.1.21] 2026-06-07
- Updated helios-core to v1.3.73 ## Context - Added `clearAllPrimitiveData(label)` and `clearAllObjectData(label)` to remove a named data field from *every* primitive/compound object in the Context (including hidden ones) and release the registered data type for that label, complementing the existing per-UUID/per-objID `clearPrimitiveData()`/`clearObjectData()`. - Added `deleteTimeseriesDataPoint(date, time, label=None)` to delete a single timeseries data point at a given date/time — for one variable when `label` is given, or across all variables when `label` is `None`. - `Location` gained an `altitude` field (meters above sea level, default 0.0); `setLocation()` accepts an optional `altitude` in its float form, `getLocation()` now returns it, and `make_Location()` accepts an optional 4th argument. Existing 3-argument usage is unchanged. Note Helios's non-standard longitude convention (+W / −E), which is auto-flipped to the standard +E convention when written into camera EXIF metadata. ## Radiation - `CameraProperties` gained a `manufacturer` field (helios-core v1.3.73 maps it to the EXIF camera *Make* tag; empty ⇒ "Helios"). Like the other `CameraProperties` string fields, it is exposed on the Python class for forward compatibility but is not yet plumbed through to the native camera. Camera images written via `writeCameraImage()` embed EXIF/XMP metadata (camera intrinsics, orientation, and GPS derived from the Context `Location`) automatically on the native side. ## LiDAR - `LiDARCloud.addScan()` gained optional `range_noise_stddev` (meters) and `angle_noise_stddev` (radians) arguments that drive realistic anisotropic positional error during `syntheticScan()` (along-beam range noise and across-beam beam-pointing jitter). Both default to 0.0 (disabled), preserving prior behavior. Query them with `getScanRangeNoiseStdDev(scanID)` / `getScanAngleNoiseStdDev(scanID)`. - Added `exportScans(filename)` to write all scans as an XML metadata file plus one ASCII data file per scan (auto-named `<base>_<scanID>.xyz`), re-loadable with `loadXML()`.
1 parent 46c2f15 commit af72e74

20 files changed

Lines changed: 1401 additions & 92 deletions

docs/CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# Changelog
22

3+
# [v0.1.21] 2026-06-07
4+
5+
- Updated helios-core to v1.3.73
6+
7+
## Context
8+
- Added `clearAllPrimitiveData(label)` and `clearAllObjectData(label)` to remove a named data field from *every* primitive/compound object in the Context (including hidden ones) and release the registered data type for that label, complementing the existing per-UUID/per-objID `clearPrimitiveData()`/`clearObjectData()`.
9+
- Added `deleteTimeseriesDataPoint(date, time, label=None)` to delete a single timeseries data point at a given date/time — for one variable when `label` is given, or across all variables when `label` is `None`.
10+
- `Location` gained an `altitude` field (meters above sea level, default 0.0); `setLocation()` accepts an optional `altitude` in its float form, `getLocation()` now returns it, and `make_Location()` accepts an optional 4th argument. Existing 3-argument usage is unchanged. Note Helios's non-standard longitude convention (+W / −E), which is auto-flipped to the standard +E convention when written into camera EXIF metadata.
11+
12+
## Radiation
13+
- `CameraProperties` gained a `manufacturer` field (helios-core v1.3.73 maps it to the EXIF camera *Make* tag; empty ⇒ "Helios"). Like the other `CameraProperties` string fields, it is exposed on the Python class for forward compatibility but is not yet plumbed through to the native camera. Camera images written via `writeCameraImage()` embed EXIF/XMP metadata (camera intrinsics, orientation, and GPS derived from the Context `Location`) automatically on the native side.
14+
15+
## LiDAR
16+
- `LiDARCloud.addScan()` gained optional `range_noise_stddev` (meters) and `angle_noise_stddev` (radians) arguments that drive realistic anisotropic positional error during `syntheticScan()` (along-beam range noise and across-beam beam-pointing jitter). Both default to 0.0 (disabled), preserving prior behavior. Query them with `getScanRangeNoiseStdDev(scanID)` / `getScanAngleNoiseStdDev(scanID)`.
17+
- Added `exportScans(filename)` to write all scans as an XML metadata file plus one ASCII data file per scan (auto-named `<base>_<scanID>.xyz`), re-loadable with `loadXML()`.
18+
319
# [v0.1.20] 2026-05-08
420

521
- Updated helios-core to v1.3.72
@@ -26,6 +42,10 @@
2642
## Radiation
2743
- Added `addSIFCamera()` (vec3 lookat and SphericalCoord overloads) plus the new `SIFCameraProperties` (extends `CameraProperties` with `excitation_bin_width_nm` and `excitation_scattering_depth`) and the `isSIFCamera()` query. SIF cameras source per-band emission from the Fluspect-B kernel rather than Stefan-Boltzmann; Helios auto-creates internal excitation bands covering 400–750 nm at the requested bin width.
2844

45+
## LiDAR
46+
- Exposed per-hit scalar data and metadata that `syntheticScan()` already computes: `LiDARCloud.getHitData(index, label)`, `doesHitDataExist(index, label)`, and `getHitScanID(index)`, reaching `intensity`, `distance`, `timestamp`, `target_index`, `target_count`, `deviation`, `nRaysHit`, and any column-format fields. Added bulk single-call exports `getHitDataAll(label)` and `getHitsXYZRGB()` for large clouds.
47+
- Generalized the synthetic scan's primitive-data → hit-data transfer to be driven by the scan's column format: any non-standard label in a scan's `column_format` is now sampled from the struck primitive (FLOAT/DOUBLE/INT/UINT) onto each hit, replacing the previously hardcoded `object_label`/`reflectivity_lidar` pair (`reflectivity_lidar` retains its intensity-modulation behavior). `LiDARCloud.addScan()` gained an optional `column_format` argument (default keeps prior behavior); the previously auto-copied `object_label` must now be listed in `column_format` to transfer.
48+
2949
# [v0.1.19] 2026-04-16
3050

3151
- Updated helios-core to v1.3.71

docs/file_io.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ The Context can automatically import Stanford .ply files [en.wikipedia.org/wiki/
140140

141141
```python
142142
from pyhelios import Context
143-
from pyhelios.types import vec3, SphericalCoord, RGBcolorcolor, RGBcolor
143+
from pyhelios.types import vec3, SphericalCoord, RGBcolor, RGBcolor
144144
import math
145145

146146
context = Context()
@@ -197,7 +197,7 @@ from pyhelios.types import vec3, RGBcolor
197197

198198
context = Context()
199199

200-
context.loadOBJ("relative/path/to/someobjfile.obj", vec3(0, 0, 0), 0, SphericalCoord(1, 0, 0), RGBcolorcolor(1, 0, 0))
200+
context.loadOBJ("relative/path/to/someobjfile.obj", vec3(0, 0, 0), 0, SphericalCoord(1, 0, 0), RGBcolor(1, 0, 0))
201201
```
202202

203203
### Writing PLY (Stanford Polygon) Files {#PLYwrite}

docs/plugin_lidar.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ Each scan has a set of parameters or "metadata" that must be specified in order
103103
<tr><td>Rotation</td><td>\<rotation></td><td>Global spherical rotation (theta,phi) to be applied to the entire scan, including the origin and all hit points.</td><td>Use `coordinateRotation()`</td><td>No rotation</td></tr>
104104
<tr><td>Beam exit diameter (meters)</td><td>\<exitDiameter></td><td>Effective diameter of laser beam exiting the instrument. Only used for full-waveform synthetic data generation.</td><td>`exit_diameter`</td><td>0 (discrete return)</td></tr>
105105
<tr><td>Beam divergence angle (rad)</td><td>\<beamDivergence></td><td>Angle of laser beam divergence after exiting the instrument. Only used for full-waveform synthetic data generation.</td><td>`beam_divergence`</td><td>0</td></tr>
106+
<tr><td>Range noise std. dev. (meters)</td><td>\<rangeNoiseStdDev></td><td>Standard deviation of zero-mean Gaussian noise added to the measured range (distance) of each return during synthetic scan generation, displacing the hit point along the beam direction (along-beam error). Only used for synthetic data generation.</td><td>`range_noise_stddev`</td><td>0 (no noise)</td></tr>
107+
<tr><td>Angular noise std. dev. (rad)</td><td>\<angleNoiseStdDev></td><td>Standard deviation of zero-mean Gaussian jitter applied to the pointing direction of each pulse during synthetic scan generation, producing the across-beam (lateral) error that grows with range. Only used for synthetic data generation.</td><td>`angle_noise_stddev`</td><td>0 (no jitter)</td></tr>
106108
<tr><td>ASCII point cloud file</td><td>\<filename></td><td>File containing point cloud data to be read.</td><td>Loaded via `loadXML()`</td><td>No file will be read</td></tr>
107109
<tr><td>ASCII file column format</td><td>\<ASCII_format></td><td>Labels for columns in ASCII point cloud file. See section below for possible values and examples.</td><td>Loaded via `loadXML()`</td><td>x y z</td></tr>
108110
</table>
@@ -498,6 +500,7 @@ Results of data processing can be easily written to file for external analysis.
498500
<table>
499501
<tr><th>Function</th><th>Description</th><th>PyHelios Status</th></tr>
500502
<tr><td>exportPointCloud(filename)</td><td>Write the entire point cloud to ASCII file.</td><td>✅ Available</td></tr>
503+
<tr><td>exportScans(filename)</td><td>Write an XML metadata file describing all scans plus one ASCII point cloud file per scan (auto-named `&lt;base&gt;_&lt;scanID&gt;.xyz`). The XML can be re-loaded with `loadXML()`.</td><td>✅ Available</td></tr>
501504
<tr><td>exportTriangleNormals(filename)</td><td>Write the unit normal vectors [nx ny nz] of all triangles formed from triangulation.</td><td>✅ Available</td></tr>
502505
<tr><td>exportTriangleAreas(filename)</td><td>Write the areas of all triangles formed from triangulation.</td><td>✅ Available</td></tr>
503506
<tr><td>exportLeafAreas(filename)</td><td>Write the leaf area contained within each voxel.</td><td>✅ Available</td></tr>

docs/plugin_solarposition.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ from pyhelios.types import *
3333

3434
with Context() as context:
3535
# Set date and time
36-
context.setDate(1, 5, 2015) # May 1, 2015
37-
context.setTime(30, 12) # 12:30
36+
context.setDate(2015, 5, 1) # May 1, 2015
37+
context.setTime(12, 30) # 12:30
3838

3939
# Create SolarPosition with location (UTC offset, latitude, longitude)
4040
with SolarPosition(7, 31.256, 119.947, context) as sun:
@@ -223,8 +223,8 @@ from pyhelios.types import *
223223

224224
with Context() as context:
225225
# Set the current time and date
226-
context.setDate(1, 5, 2015) # May 1, 2015
227-
context.setTime(30, 12) # 12:30
226+
context.setDate(2015, 5, 1) # May 1, 2015
227+
context.setTime(12, 30) # 12:30
228228

229229
# Initialize the SolarPosition class with coordinates
230230
# Arguments: context, utc_offset, latitude, longitude
@@ -250,8 +250,8 @@ import math
250250

251251
with Context() as context:
252252
# Set the current time and date
253-
context.setDate(1, 5, 2015) # May 1, 2015
254-
context.setTime(30, 12) # 12:30
253+
context.setDate(2015, 5, 1) # May 1, 2015
254+
context.setTime(12, 30) # 12:30
255255

256256
# Initialize the SolarPosition class
257257
with SolarPosition(context, 7, 31.256, 119.947) as sun:
@@ -327,8 +327,8 @@ import math
327327

328328
with Context() as context:
329329
# Set the current time and date
330-
context.setDate(1, 5, 2015) # May 1, 2015
331-
context.setTime(30, 12) # 12:30
330+
context.setDate(2015, 5, 1) # May 1, 2015
331+
context.setTime(12, 30) # 12:30
332332

333333
# Initialize the SolarPosition class
334334
with SolarPosition(context, 7, 31.256, 119.947) as sun:
@@ -440,7 +440,7 @@ from pyhelios import Context, SolarPosition
440440

441441
with Context() as context:
442442
# Set current time, date, and location
443-
context.setDate(16, 7, 2023) # July 16, 2023
443+
context.setDate(2023, 7, 16) # July 16, 2023
444444
context.setTime(12, 0) # Solar noon
445445

446446
# Initialize SolarPosition with location

docs/user_guide.md

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -34,35 +34,35 @@ Vector types can be initialized directly. For example, `i2 = int2(1, 2)` creates
3434

3535
### R-G-B(-A) color vectors
3636

37-
There are several predefined RGB color vectors (see RGBcolor) that can be used, which are tabulated below:
38-
39-
| Color | RGB Values | Color Swatch |
40-
|-------|------------|--------------|
41-
| RGB::black | (0,0,0) | <div style="width:50px;height:30px;background-color:rgb(0,0,0);border:1px solid #ccc;"></div> |
42-
| RGB::white | (1,1,1) | <div style="width:50px;height:30px;background-color:rgb(255,255,255);border:1px solid #ccc;"></div> |
43-
| RGB::red | (1,0,0) | <div style="width:50px;height:30px;background-color:rgb(255,0,0);"></div> |
44-
| RGB::blue | (0,0,1) | <div style="width:50px;height:30px;background-color:rgb(0,0,255);"></div> |
45-
| RGB::green | (0,0.6,0) | <div style="width:50px;height:30px;background-color:rgb(0,153,0);"></div> |
46-
| RGB::cyan | (0,1,1) | <div style="width:50px;height:30px;background-color:rgb(0,255,255);"></div> |
47-
| RGB::magenta | (1,0,1) | <div style="width:50px;height:30px;background-color:rgb(255,0,255);"></div> |
48-
| RGB::yellow | (1,1,0) | <div style="width:50px;height:30px;background-color:rgb(255,255,0);"></div> |
49-
| RGB::orange | (1,0.5,0) | <div style="width:50px;height:30px;background-color:rgb(255,127,0);"></div> |
50-
| RGB::violet | (0.5,0,0.5) | <div style="width:50px;height:30px;background-color:rgb(127,0,127);"></div> |
51-
| RGB::lime | (0,1,0) | <div style="width:50px;height:30px;background-color:rgb(0,255,0);"></div> |
52-
| RGB::silver | (0.75,0.75,0.75) | <div style="width:50px;height:30px;background-color:rgb(191,191,191);"></div> |
53-
| RGB::gray | (0.5,0.5,0.5) | <div style="width:50px;height:30px;background-color:rgb(127,127,127);"></div> |
54-
| RGB::navy | (0,0,0.5) | <div style="width:50px;height:30px;background-color:rgb(0,0,127);"></div> |
55-
| RGB::brown | (0.55,0.27,0.075) | <div style="width:50px;height:30px;background-color:rgb(140,69,19);"></div> |
56-
| RGB::khaki | (0.94,0.92,0.55) | <div style="width:50px;height:30px;background-color:rgb(240,235,140);"></div> |
57-
| RGB::greenyellow | (0.678,1,0.184) | <div style="width:50px;height:30px;background-color:rgb(173,255,47);"></div> |
58-
| RGB::forestgreen | (0.133,0.545,0.133) | <div style="width:50px;height:30px;background-color:rgb(34,139,34);"></div> |
59-
| RGB::yellowgreen | (0.6,0.8,0.2) | <div style="width:50px;height:30px;background-color:rgb(153,204,51);"></div> |
60-
| RGB::goldenrod | (0.855,0.647,0.126) | <div style="width:50px;height:30px;background-color:rgb(218,165,32);"></div> |
61-
62-
Note that the above colors can be directly used with RGBAcolor to specify an alpha (transparency) value:
37+
PyHelios does not provide predefined named color constants; construct colors directly with `RGBcolor(r, g, b)` (values normalized to 1). The table below lists common colors and their RGB values for convenience:
38+
39+
| Color | RGB Values | `RGBcolor(...)` | Color Swatch |
40+
|-------|------------|-----------------|--------------|
41+
| black | (0,0,0) | `RGBcolor(0, 0, 0)` | <div style="width:50px;height:30px;background-color:rgb(0,0,0);border:1px solid #ccc;"></div> |
42+
| white | (1,1,1) | `RGBcolor(1, 1, 1)` | <div style="width:50px;height:30px;background-color:rgb(255,255,255);border:1px solid #ccc;"></div> |
43+
| red | (1,0,0) | `RGBcolor(1, 0, 0)` | <div style="width:50px;height:30px;background-color:rgb(255,0,0);"></div> |
44+
| blue | (0,0,1) | `RGBcolor(0, 0, 1)` | <div style="width:50px;height:30px;background-color:rgb(0,0,255);"></div> |
45+
| green | (0,0.6,0) | `RGBcolor(0, 0.6, 0)` | <div style="width:50px;height:30px;background-color:rgb(0,153,0);"></div> |
46+
| cyan | (0,1,1) | `RGBcolor(0, 1, 1)` | <div style="width:50px;height:30px;background-color:rgb(0,255,255);"></div> |
47+
| magenta | (1,0,1) | `RGBcolor(1, 0, 1)` | <div style="width:50px;height:30px;background-color:rgb(255,0,255);"></div> |
48+
| yellow | (1,1,0) | `RGBcolor(1, 1, 0)` | <div style="width:50px;height:30px;background-color:rgb(255,255,0);"></div> |
49+
| orange | (1,0.5,0) | `RGBcolor(1, 0.5, 0)` | <div style="width:50px;height:30px;background-color:rgb(255,127,0);"></div> |
50+
| violet | (0.5,0,0.5) | `RGBcolor(0.5, 0, 0.5)` | <div style="width:50px;height:30px;background-color:rgb(127,0,127);"></div> |
51+
| lime | (0,1,0) | `RGBcolor(0, 1, 0)` | <div style="width:50px;height:30px;background-color:rgb(0,255,0);"></div> |
52+
| silver | (0.75,0.75,0.75) | `RGBcolor(0.75, 0.75, 0.75)` | <div style="width:50px;height:30px;background-color:rgb(191,191,191);"></div> |
53+
| gray | (0.5,0.5,0.5) | `RGBcolor(0.5, 0.5, 0.5)` | <div style="width:50px;height:30px;background-color:rgb(127,127,127);"></div> |
54+
| navy | (0,0,0.5) | `RGBcolor(0, 0, 0.5)` | <div style="width:50px;height:30px;background-color:rgb(0,0,127);"></div> |
55+
| brown | (0.55,0.27,0.075) | `RGBcolor(0.55, 0.27, 0.075)` | <div style="width:50px;height:30px;background-color:rgb(140,69,19);"></div> |
56+
| khaki | (0.94,0.92,0.55) | `RGBcolor(0.94, 0.92, 0.55)` | <div style="width:50px;height:30px;background-color:rgb(240,235,140);"></div> |
57+
| greenyellow | (0.678,1,0.184) | `RGBcolor(0.678, 1, 0.184)` | <div style="width:50px;height:30px;background-color:rgb(173,255,47);"></div> |
58+
| forestgreen | (0.133,0.545,0.133) | `RGBcolor(0.133, 0.545, 0.133)` | <div style="width:50px;height:30px;background-color:rgb(34,139,34);"></div> |
59+
| yellowgreen | (0.6,0.8,0.2) | `RGBcolor(0.6, 0.8, 0.2)` | <div style="width:50px;height:30px;background-color:rgb(153,204,51);"></div> |
60+
| goldenrod | (0.855,0.647,0.126) | `RGBcolor(0.855, 0.647, 0.126)` | <div style="width:50px;height:30px;background-color:rgb(218,165,32);"></div> |
61+
62+
Any color can be used with RGBAcolor to specify an alpha (transparency) value:
6363

6464
```python
65-
from pyhelios.types import RGBAcolor, RGBcolorcolor
65+
from pyhelios.types import RGBAcolor, RGBcolor
6666

6767
# Create red color first, then use its components
6868
red = RGBcolor(1, 0, 0)
@@ -106,7 +106,7 @@ The Context is usually passed to plugins, which gives them access to geometry an
106106

107107
### Geographic Location {#Location}
108108

109-
The Context stores a geographic location used by plugins such as SolarPosition and RadiationModel for sun-position calculations. Location is represented by the immutable `Location` dataclass with three fields: `latitude` in degrees (+N / -S), `longitude` in degrees (+W / -E per Helios convention), and `utc_offset` in hours (+ moving West).
109+
The Context stores a geographic location used by plugins such as SolarPosition and RadiationModel for sun-position calculations. Location is represented by the immutable `Location` dataclass with four fields: `latitude` in degrees (+N / -S), `longitude` in degrees (+W / -E per Helios convention), `utc_offset` in hours (+ moving West), and `altitude` in meters above sea level (default 0.0). The non-standard +W longitude convention is automatically flipped to the standard +E convention when written into camera EXIF metadata.
110110

111111
```python
112112
from pyhelios import Context
@@ -115,12 +115,13 @@ from pyhelios.types import Location
115115
context = Context()
116116

117117
# Set location explicitly
118-
context.setLocation(38.55, 121.76, 8.0) # latitude, longitude, UTC offset
119-
context.setLocation(Location(38.55, 121.76, 8.0)) # equivalent
118+
context.setLocation(38.55, 121.76, 8.0) # latitude, longitude, UTC offset
119+
context.setLocation(38.55, 121.76, 8.0, altitude=16.0) # with altitude (meters)
120+
context.setLocation(Location(38.55, 121.76, 8.0, 16.0)) # equivalent
120121

121122
# Read it back
122123
loc = context.getLocation()
123-
print(loc.latitude, loc.longitude, loc.utc_offset)
124+
print(loc.latitude, loc.longitude, loc.utc_offset, loc.altitude)
124125
```
125126

126127
Once the Context location is set, downstream plugins can read it instead of taking explicit lat/lon arguments.
@@ -203,15 +204,15 @@ Triangles are added by specifying the (x,y,z) coordinates of the triangle's thre
203204

204205
```python
205206
from pyhelios import Context
206-
from pyhelios.types import vec3, RGBcolorcolor
207+
from pyhelios.types import vec3, RGBcolor
207208

208209
context = Context()
209210

210211
v0 = vec3(-0.5, -0.5, 1)
211212
v1 = vec3(0.5, -0.5, 1)
212213
v2 = vec3(0, 0.5, 1)
213214

214-
UUID = context.addTriangle(v0, v1, v2, RGBcolorcolor(1, 0, 0))
215+
UUID = context.addTriangle(v0, v1, v2, RGBcolor(1, 0, 0))
215216
```
216217

217218
This will add the Triangle shown below. (Note that the addition of the checkerboard ground and the 'Visualizer' plugin is needed to replicate this image, which is not shown in the example code.)
@@ -230,7 +231,7 @@ For box-shaped geometry, use:
230231

231232
```python
232233
from pyhelios import Context
233-
from pyhelios.types import vec3, int3, RGBcolorcolor
234+
from pyhelios.types import vec3, int3, RGBcolor
234235

235236
context = Context()
236237

@@ -239,7 +240,7 @@ size = vec3(1, 1, 1)
239240
subdivisions = int3(1, 1, 1)
240241

241242
# Use addBox for voxel-like geometry
242-
UUIDs = context.addBox(center, size, subdivisions, RGBcolorcolor(1, 0, 0))
243+
UUIDs = context.addBox(center, size, subdivisions, RGBcolor(1, 0, 0))
243244
```
244245

245246
![Voxel geometric primitive](images/primitives/Voxel.png)
@@ -500,14 +501,14 @@ Primitive data values can be scalar or a one-dimensional array of values.
500501

501502
```python
502503
from pyhelios import Context
503-
from pyhelios.types import vec3, vec2, RGBcolorcolor
504+
from pyhelios.types import vec3, vec2, RGBcolor
504505

505506
context = Context()
506507

507508
center = vec3(0, 0, 0)
508509
size = vec2(1, 1)
509510

510-
UUID = context.addPatch(center, size, RGBcolorcolor(1, 0, 0))
511+
UUID = context.addPatch(center, size, RGBcolor(1, 0, 0))
511512

512513
eps = 0.9
513514
context.setPrimitiveDataFloat(UUID, "emissivity", eps)

helios-core

Submodule helios-core updated 72 files

0 commit comments

Comments
 (0)