Skip to content

Commit 1479bfb

Browse files
committed
[v0.1.9] 2025-11-27
- Updated helios-core to v1.3.57 - There was a memory leak issue in the core and all plug-ins due to missing `__del__` methods, which should be fixed now. ## Core - Added overloaded `Context.setPrimitiveData[*]()` to accept a list of UUIDs - Added `Context.deletePrimitive()` and `Context.deleteObject()` methods - Added `Context.writePrimitiveData()` method to write primitive data to a file ## Radiation Model - Added new radiation source types: `addRectangleRadiationSource()`, `addDiskRadiationSource()` - Added source management: `setSourcePosition()`, `getSourcePosition()`, `deleteRadiationSource()` - Added spectrum manipulation: `setSourceSpectrum()`, `integrateSpectrum()`, `scaleSpectrum()`, `blendSpectra()` - Added diffuse radiation support: `setDiffuseRadiationExtinctionCoeff()`, `setDiffuseSpectrum()`, `getDiffuseFlux()` - Added camera system: position, lookat, orientation, spectral response, and pixel data methods - Added utility methods: `doesBandExist()`, `getSkyEnergy()`, `calculateGtheta()`, `enforcePeriodicBoundary()` - Extended `copyRadiationBand()` to support optional wavelength range parameters
1 parent 04c95b8 commit 1479bfb

27 files changed

Lines changed: 7596 additions & 165 deletions

.github/workflows/docs.yml

Lines changed: 195 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,107 @@ jobs:
7878
run: |
7979
# Create output directory
8080
mkdir -p docs/generated
81-
81+
8282
# Build documentation with Doxygen
8383
doxygen docs/Doxyfile.python
84-
84+
85+
# Create .nojekyll file to disable Jekyll processing on GitHub Pages
86+
# This is critical because Jekyll ignores files starting with underscores
87+
# which breaks Doxygen navigation (many generated files start with _)
88+
touch docs/generated/html/.nojekyll
89+
8590
# Verify documentation was generated
8691
ls -la docs/generated/html/
8792
echo "✓ Documentation built successfully"
88-
93+
94+
- name: Validate Doxygen artifacts
95+
run: |
96+
echo "=== Doxygen Documentation Validation ==="
97+
98+
VALIDATION_FAILED=0
99+
DOC_DIR="docs/generated/html"
100+
101+
# 1. Check critical files
102+
echo ""
103+
echo "[1/5] Checking critical files..."
104+
REQUIRED_FILES=(
105+
"index.html"
106+
"doxygen-awesome.css"
107+
".nojekyll"
108+
"search/search.js"
109+
)
110+
111+
for file in "${REQUIRED_FILES[@]}"; do
112+
if [ ! -f "$DOC_DIR/$file" ]; then
113+
echo " ERROR: MISSING: $file"
114+
VALIDATION_FAILED=1
115+
else
116+
size=$(stat -c%s "$DOC_DIR/$file" 2>/dev/null)
117+
echo " OK: $file (${size} bytes)"
118+
fi
119+
done
120+
121+
# 2. Validate index.html structure
122+
echo ""
123+
echo "[2/5] Validating index.html structure..."
124+
if [ -f "$DOC_DIR/index.html" ]; then
125+
if grep -q "<title>" "$DOC_DIR/index.html" && \
126+
grep -q "</body>" "$DOC_DIR/index.html"; then
127+
echo " OK: index.html appears well-formed"
128+
else
129+
echo " ERROR: index.html appears malformed"
130+
VALIDATION_FAILED=1
131+
fi
132+
fi
133+
134+
# 3. Check for underscore files (should work with .nojekyll)
135+
echo ""
136+
echo "[3/5] Checking for Doxygen underscore files..."
137+
UNDERSCORE_COUNT=$(find "$DOC_DIR" -name "_*.js" -o -name "_*.html" | wc -l)
138+
if [ "$UNDERSCORE_COUNT" -gt 0 ]; then
139+
echo " OK: Found $UNDERSCORE_COUNT underscore files (normal for Doxygen)"
140+
if [ ! -f "$DOC_DIR/.nojekyll" ]; then
141+
echo " ERROR: .nojekyll missing - these files will be ignored by Jekyll!"
142+
VALIDATION_FAILED=1
143+
fi
144+
fi
145+
146+
# 4. Verify directory structure
147+
echo ""
148+
echo "[4/5] Checking directory structure..."
149+
for dir in "search" "classes" "files"; do
150+
if [ -d "$DOC_DIR/$dir" ]; then
151+
count=$(find "$DOC_DIR/$dir" -type f | wc -l)
152+
echo " OK: $dir/ ($count files)"
153+
fi
154+
done
155+
156+
# 5. Size check (GitHub Pages limit: 1GB recommended, 10GB max)
157+
echo ""
158+
echo "[5/5] Checking total size..."
159+
TOTAL_SIZE=$(du -sk "$DOC_DIR" | cut -f1)
160+
TOTAL_MB=$((TOTAL_SIZE / 1024))
161+
echo " Total size: ${TOTAL_MB} MB"
162+
163+
if [ "$TOTAL_SIZE" -gt 1048576 ]; then # 1GB in KB
164+
echo " WARNING: Size exceeds 1GB (GitHub Pages recommended limit)"
165+
elif [ "$TOTAL_SIZE" -gt 10485760 ]; then # 10GB in KB
166+
echo " ERROR: Size exceeds 10GB (GitHub Pages absolute limit)"
167+
VALIDATION_FAILED=1
168+
else
169+
echo " OK: Size within limits"
170+
fi
171+
172+
# Final verdict
173+
echo ""
174+
echo "=== Validation Complete ==="
175+
if [ $VALIDATION_FAILED -eq 1 ]; then
176+
echo "VALIDATION FAILED"
177+
exit 1
178+
else
179+
echo "ALL CHECKS PASSED"
180+
fi
181+
89182
- name: Setup Pages
90183
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main'
91184
uses: actions/configure-pages@v4
@@ -98,13 +191,110 @@ jobs:
98191

99192
deploy-docs:
100193
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main'
194+
outputs:
195+
page_url: ${{ steps.deployment.outputs.page_url }}
101196
environment:
102197
name: github-pages
103198
url: ${{ steps.deployment.outputs.page_url }}
104199
runs-on: ubuntu-latest
105200
needs: build-docs
106-
201+
107202
steps:
108203
- name: Deploy to GitHub Pages
109204
id: deployment
110-
uses: actions/deploy-pages@v4
205+
uses: actions/deploy-pages@v4
206+
207+
verify-deployment:
208+
name: Verify Deployment
209+
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main'
210+
runs-on: ubuntu-latest
211+
needs: deploy-docs
212+
213+
steps:
214+
- name: Wait for GitHub Pages propagation and verify
215+
timeout-minutes: 10
216+
continue-on-error: true
217+
id: verify
218+
run: |
219+
SITE_URL="${{ needs.deploy-docs.outputs.page_url }}"
220+
echo "=== GitHub Pages Deployment Verification ==="
221+
echo "Target URL: $SITE_URL"
222+
echo ""
223+
224+
MAX_ATTEMPTS=15
225+
RETRY_DELAY=30
226+
ATTEMPT=0
227+
228+
echo "Note: GitHub Pages CDN propagation typically takes 1-10 minutes"
229+
echo "Will retry up to $MAX_ATTEMPTS times with ${RETRY_DELAY}s delays"
230+
echo ""
231+
232+
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
233+
ATTEMPT=$((ATTEMPT + 1))
234+
echo "[$ATTEMPT/$MAX_ATTEMPTS] Checking site accessibility..."
235+
236+
# Attempt to fetch the site
237+
HTTP_CODE=$(curl -s -o /tmp/page.html -w "%{http_code}" \
238+
--connect-timeout 10 \
239+
--max-time 30 \
240+
"$SITE_URL" || echo "000")
241+
242+
case "$HTTP_CODE" in
243+
200)
244+
echo " OK: Site returned HTTP 200"
245+
246+
# Verify it's actually our documentation
247+
if grep -q "doxygen" /tmp/page.html || grep -q "PyHelios" /tmp/page.html; then
248+
echo " OK: Content verified (PyHelios documentation detected)"
249+
250+
# Check for common Doxygen elements
251+
if grep -q "search" /tmp/page.html; then
252+
echo " OK: Search functionality present"
253+
fi
254+
255+
echo ""
256+
echo "DEPLOYMENT VERIFIED SUCCESSFULLY"
257+
exit 0
258+
else
259+
echo " WARNING: Page returned 200 but doesn't appear to be PyHelios docs"
260+
echo " Continuing retries..."
261+
fi
262+
;;
263+
404)
264+
echo " Site not found (HTTP 404) - waiting for propagation..."
265+
;;
266+
000)
267+
echo " Connection failed - waiting for deployment..."
268+
;;
269+
*)
270+
echo " Received HTTP $HTTP_CODE - waiting..."
271+
;;
272+
esac
273+
274+
if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then
275+
echo " Waiting ${RETRY_DELAY}s before retry..."
276+
echo ""
277+
sleep $RETRY_DELAY
278+
fi
279+
done
280+
281+
echo ""
282+
echo "VERIFICATION FAILED"
283+
echo "Site did not become accessible after $MAX_ATTEMPTS attempts"
284+
echo ""
285+
echo "This may indicate:"
286+
echo " - Deployment is still propagating (can take up to 20-30 minutes)"
287+
echo " - GitHub Pages service issues"
288+
echo " - Intermittent CDN problems (try re-running the workflow)"
289+
echo ""
290+
echo "Manual verification recommended: $SITE_URL"
291+
exit 1
292+
293+
- name: Report verification result
294+
if: always()
295+
run: |
296+
if [ "${{ steps.verify.outcome }}" == "success" ]; then
297+
echo "::notice::Documentation successfully deployed and verified at ${{ needs.deploy-docs.outputs.page_url }}"
298+
else
299+
echo "::warning::Deployment verification failed or timed out. Site may still be propagating. Manual check: ${{ needs.deploy-docs.outputs.page_url }}"
300+
fi

docs/CHANGELOG.md

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

3+
# [v0.1.9] 2025-11-27
4+
5+
- Updated helios-core to v1.3.57
6+
- There was a memory leak issue in the core and all plug-ins due to missing `__del__` methods, which should be fixed now.
7+
8+
## Core
9+
- Added overloaded `Context.setPrimitiveData[*]()` to accept a list of UUIDs
10+
- Added `Context.deletePrimitive()` and `Context.deleteObject()` methods
11+
- Added `Context.writePrimitiveData()` method to write primitive data to a file
12+
13+
## Radiation Model
14+
- Added new radiation source types: `addRectangleRadiationSource()`, `addDiskRadiationSource()`
15+
- Added source management: `setSourcePosition()`, `getSourcePosition()`, `deleteRadiationSource()`
16+
- Added spectrum manipulation: `setSourceSpectrum()`, `integrateSpectrum()`, `scaleSpectrum()`, `blendSpectra()`
17+
- Added diffuse radiation support: `setDiffuseRadiationExtinctionCoeff()`, `setDiffuseSpectrum()`, `getDiffuseFlux()`
18+
- Added camera system: position, lookat, orientation, spectral response, and pixel data methods
19+
- Added utility methods: `doesBandExist()`, `getSkyEnergy()`, `calculateGtheta()`, `enforcePeriodicBoundary()`
20+
- Extended `copyRadiationBand()` to support optional wavelength range parameters
21+
322
# [v0.1.8] 2025-10-15
423

524
- Updated helios-core to v1.3.55

docs/plugin_integration_guide.md

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,64 @@ from . import UYourPluginWrapper
644644
- `RadiationModel.addRadiationBand()` matches C++ `RadiationModel::addRadiationBand()`
645645
- `WeberPennTree.buildTree()` matches C++ `WeberPennTree::buildTree()`
646646

647-
### 5.2 Create High-Level Class
647+
### 5.2 Mandatory `__del__` Method Requirement
648+
649+
**CRITICAL FOR MEMORY LEAK PREVENTION**: All plugin classes managing C++ resources MUST implement a `__del__` method as a fallback destructor.
650+
651+
**Why This is Mandatory**:
652+
- Python only calls `__exit__` when using `with` statements
653+
- If users don't use `with` statements, C++ destructors are NEVER called
654+
- Objects accumulate in memory until Python's garbage collector runs (much later)
655+
- This causes linear memory growth in loops, especially time series simulations
656+
657+
**The Problem**:
658+
```python
659+
for timestep in range(100):
660+
context = Context() # No 'with' statement
661+
plugin = YourPlugin(context) # No 'with' statement
662+
# ... use ...
663+
# Objects sit in memory - C++ destructors not called!
664+
# Memory accumulates linearly
665+
```
666+
667+
**The Solution**:
668+
```python
669+
def __del__(self):
670+
"""MANDATORY: Destructor to ensure C++ resources freed even without 'with' statement."""
671+
if hasattr(self, '_plugin_ptr') and self._plugin_ptr is not None:
672+
try:
673+
plugin_wrapper.destroyYourPlugin(self._plugin_ptr)
674+
self._plugin_ptr = None
675+
except Exception as e:
676+
import warnings
677+
warnings.warn(f"Error in YourPlugin.__del__: {e}")
678+
```
679+
680+
**Key Implementation Details**:
681+
1. **Always use `hasattr()`** - `__del__` might be called during partially-initialized objects
682+
2. **Check for `None`** - Prevent attempting to destroy already-cleaned-up resources
683+
3. **Use try/except** - `__del__` must NEVER raise exceptions
684+
4. **Use warnings.warn()** - Inform users of cleanup errors without crashing
685+
5. **Set pointer to None** - Prevent double-deletion if `__del__` called multiple times
686+
687+
**Testing `__del__` Implementation**:
688+
```python
689+
def test_cleanup_without_with_statement():
690+
"""Verify __del__ provides fallback cleanup."""
691+
import gc
692+
693+
context = Context()
694+
plugin = YourPlugin(context) # No 'with' statement
695+
696+
# Delete and force garbage collection
697+
del plugin
698+
del context
699+
gc.collect()
700+
701+
# If __del__ works correctly, no warnings and memory freed
702+
```
703+
704+
### 5.3 Create High-Level Class
648705

649706
**File**: `pyhelios/YourPlugin.py`
650707

@@ -753,7 +810,17 @@ class YourPlugin:
753810
if hasattr(self, '_plugin_ptr') and self._plugin_ptr:
754811
plugin_wrapper.destroyYourPlugin(self._plugin_ptr)
755812
self._plugin_ptr = None
756-
813+
814+
def __del__(self):
815+
"""MANDATORY: Destructor to ensure C++ resources freed even without 'with' statement."""
816+
if hasattr(self, '_plugin_ptr') and self._plugin_ptr is not None:
817+
try:
818+
plugin_wrapper.destroyYourPlugin(self._plugin_ptr)
819+
self._plugin_ptr = None
820+
except Exception as e:
821+
import warnings
822+
warnings.warn(f"Error in YourPlugin.__del__: {e}")
823+
757824
def computeSomething(self, parameters: List[float]) -> int:
758825
"""
759826
Perform plugin computation with given parameters.

0 commit comments

Comments
 (0)