Skip to content

Commit 8243452

Browse files
authored
Merge branch 'main' into main
2 parents d3c60db + ba3385b commit 8243452

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+16349
-2204
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
name: Telemetry Badge
2+
3+
on:
4+
schedule:
5+
- cron: "0 6 * * *" # daily at 06:00 UTC
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
update-badge:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Check out repository
16+
uses: actions/checkout@v4
17+
18+
- name: Generate telemetry badge
19+
env:
20+
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
21+
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
22+
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
23+
BADGE_PATH: docs/badges/telemetry.json
24+
run: |
25+
set -euo pipefail
26+
if grep -q "__BADGE_REPO__" README.md; then
27+
sed -i "s#__BADGE_REPO__#${GITHUB_REPOSITORY}#g" README.md
28+
fi
29+
python - <<'PY'
30+
import json
31+
import os
32+
import sys
33+
import urllib.parse
34+
import urllib.error
35+
import urllib.request
36+
from pathlib import Path
37+
38+
token = os.environ.get("SENTRY_AUTH_TOKEN", "").strip()
39+
org = os.environ.get("SENTRY_ORG", "").strip()
40+
project = os.environ.get("SENTRY_PROJECT", "").strip()
41+
badge_path = Path(os.environ.get("BADGE_PATH", "docs/badges/telemetry.json"))
42+
43+
if not token or not org or not project:
44+
sys.exit("Sentry credentials (SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT) are required.")
45+
46+
def api_get(url: str):
47+
headers = {
48+
"Authorization": f"Bearer {token}",
49+
"Content-Type": "application/json",
50+
"Accept": "application/json",
51+
"User-Agent": "svv-telemetry-badge",
52+
}
53+
req = urllib.request.Request(url, headers=headers)
54+
with urllib.request.urlopen(req, timeout=20) as resp:
55+
data = resp.read()
56+
return json.loads(data.decode()), resp.headers
57+
58+
def latest_release_version():
59+
url = f"https://sentry.io/api/0/projects/{org}/{project}/releases/?per_page=1"
60+
payload, _ = api_get(url)
61+
if not payload:
62+
return None
63+
return payload[0].get("version")
64+
65+
def unresolved_count(version: str | None, proj_id: str | None):
66+
"""
67+
Return unresolved issue count. Tries release-specific first, then
68+
global unresolved. If both fail, return 0 but log the failure.
69+
"""
70+
queries = []
71+
if version:
72+
queries.append(f'is:unresolved release:"{version}"')
73+
queries.append("is:unresolved")
74+
75+
for query in queries:
76+
params_dict = {"query": query, "statsPeriod": "24h", "limit": 1}
77+
params = urllib.parse.urlencode(params_dict)
78+
url = f"https://sentry.io/api/0/projects/{org}/{project}/issues/?{params}"
79+
try:
80+
payload, headers = api_get(url)
81+
hits = headers.get("X-Hits")
82+
if hits is not None:
83+
try:
84+
return int(hits)
85+
except Exception:
86+
pass
87+
return len(payload)
88+
except urllib.error.HTTPError as err:
89+
body = err.read().decode(errors="ignore")
90+
print(f"[telemetry-badge] query failed ({err.code}): {err.reason} | body={body}")
91+
continue
92+
except Exception as exc:
93+
print(f"[telemetry-badge] query error: {exc}")
94+
continue
95+
96+
# If all attempts failed, avoid breaking the workflow.
97+
return 0
98+
99+
version = latest_release_version()
100+
count = unresolved_count(version, None)
101+
102+
if count == 0:
103+
color = "brightgreen"
104+
elif count < 5:
105+
color = "yellow"
106+
elif count < 15:
107+
color = "orange"
108+
else:
109+
color = "red"
110+
111+
message_suffix = version if version else "no release"
112+
badge = {
113+
"schemaVersion": 1,
114+
"label": "telemetry",
115+
"message": f"{count} unresolved | {message_suffix}",
116+
"color": color,
117+
}
118+
119+
badge_path.parent.mkdir(parents=True, exist_ok=True)
120+
badge_path.write_text(json.dumps(badge), encoding="utf-8")
121+
print(f"Wrote badge to {badge_path} with count={count}, version={message_suffix}")
122+
PY
123+
124+
- name: Commit badge update
125+
uses: stefanzweifel/git-auto-commit-action@v5
126+
with:
127+
commit_message: "chore: update telemetry badge"
128+
file_pattern: docs/badges/telemetry.json README.md

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
[![codecov](https://codecov.io/github/SimVascular/svVascularize/graph/badge.svg)](https://codecov.io/github/SimVascular/svVascularize)
88
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15151168.svg)](https://doi.org/10.5281/zenodo.15151168)
99
[![Docs](https://img.shields.io/badge/docs-gh--pages-brightgreen)](https://simvascular.github.io/svVascularize/)
10+
[![Telemetry](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zasexton/svVascularize/main/docs/badges/telemetry.json)](https://sentry.io)
1011
<!-- smoke-test-badge -->
1112
[![SVV passing](https://img.shields.io/badge/svv_passing-linux_ok-macos_ok-windows_ok-brightgreen)](https://github.com/SimVascular/svVascularize/actions/workflows/basic-smoke-test.yml?query=branch%3Amain)
1213
<!-- /smoke-test-badge -->

docs/api/domain.html

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ <h4 class="sidebar-title">Contents</h4>
9999
<li><a href="#constructor"><i class="fas fa-hammer"></i> Constructor</a></li>
100100
<li><a href="#attributes"><i class="fas fa-database"></i> Attributes</a></li>
101101
<li><a href="#methods"><i class="fas fa-cogs"></i> Methods</a></li>
102+
<li><a href="#persistence"><i class="fas fa-save"></i> Persistence</a></li>
102103
<li><a href="#operators"><i class="fas fa-calculator"></i> Operators</a></li>
103104
<li><a href="#examples"><i class="fas fa-code"></i> Examples</a></li>
104105
<li><a href="#notes"><i class="fas fa-info-circle"></i> Notes</a></li>
@@ -440,6 +441,87 @@ <h3>Utility Methods</h3>
440441
</div>
441442
</section>
442443

444+
<!-- Persistence Section -->
445+
<section id="persistence" class="api-section">
446+
<h2>Persistence (Save/Load)</h2>
447+
448+
<p>Domains can be saved to and loaded from <code>.dmn</code> files, allowing you to preserve
449+
expensive implicit function computations and share domain configurations.</p>
450+
451+
<div class="api-method">
452+
<div class="api-method-signature">
453+
<code>save(path, include_boundary=False, include_mesh=False, include_patch_normals=True)</code>
454+
</div>
455+
<p>Save the domain to a <code>.dmn</code> file.</p>
456+
<div class="api-method-params">
457+
<h4>Parameters</h4>
458+
<ul>
459+
<li><code>path</code> (str): Output filename. If no extension is provided, <code>.dmn</code> is appended.</li>
460+
<li><code>include_boundary</code> (bool): Persist the boundary mesh for visualization without recomputation. Default is False.</li>
461+
<li><code>include_mesh</code> (bool): Persist the interior mesh for sampling without recomputation. Default is False.</li>
462+
<li><code>include_patch_normals</code> (bool): Include per-patch normals to fully restore Patch objects. Default is True.</li>
463+
</ul>
464+
<h4>Notes</h4>
465+
<p>The saved file contains the implicit function coefficients and patch data needed to
466+
reconstruct the domain. Including boundary and mesh data increases file size but
467+
speeds up subsequent operations.</p>
468+
</div>
469+
</div>
470+
471+
<div class="api-method">
472+
<div class="api-method-signature">
473+
<code>Domain.load(path)</code> <span class="api-badge">classmethod</span>
474+
</div>
475+
<p>Load a domain from a <code>.dmn</code> file.</p>
476+
<div class="api-method-params">
477+
<h4>Parameters</h4>
478+
<ul>
479+
<li><code>path</code> (str): Path to a <code>.dmn</code> file.</li>
480+
</ul>
481+
<h4>Returns</h4>
482+
<ul>
483+
<li><code>Domain</code>: Fully initialized Domain instance ready for fast evaluation.</li>
484+
</ul>
485+
<h4>Notes</h4>
486+
<p>The loaded domain has its <code>function_tree</code> rebuilt and is ready for use
487+
with <code>__call__</code> (fast evaluation). Patches are reconstructed from stored
488+
arrays, enabling you to call <code>build()</code> again if needed.</p>
489+
</div>
490+
</div>
491+
492+
<div class="api-example">
493+
<h4>Save/Load Example</h4>
494+
<pre data-copy><code class="language-python">from svv.domain.domain import Domain
495+
import pyvista as pv
496+
497+
# Create domain from a complex mesh
498+
mesh = pv.read("heart_ventricle.stl")
499+
domain = Domain(mesh)
500+
domain.create() # Creates patches (can be slow for complex meshes)
501+
domain.solve() # Solves RBF system (expensive)
502+
domain.build() # Builds boundary and mesh
503+
504+
# Save domain for later use
505+
domain.save("ventricle.dmn", include_boundary=True, include_mesh=True)
506+
507+
# Later, load the domain instantly
508+
loaded_domain = Domain.load("ventricle.dmn")
509+
510+
# Use immediately - no need to call create/solve/build
511+
point = [[0.5, 0.5, 0.5]]
512+
inside = loaded_domain(point) < 0 # Fast evaluation works immediately
513+
514+
# Or call build() if you need fresh boundary/mesh
515+
loaded_domain.build()</code></pre>
516+
</div>
517+
518+
<div class="callout tip">
519+
<strong>Performance Tip:</strong> For complex domains, saving with <code>include_boundary=True</code>
520+
and <code>include_mesh=True</code> significantly speeds up subsequent loads at the cost of
521+
larger file sizes. This is especially useful when iterating on tree generation with the same domain.
522+
</div>
523+
</section>
524+
443525
<!-- Operators Section -->
444526
<section id="operators" class="api-section">
445527
<h2>Operators</h2>

docs/api/forest.html

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ <h4 class="sidebar-title">Contents</h4>
121121
<li><a href="#growth-methods"><i class="fas fa-seedling"></i> Growth Methods</a></li>
122122
<li><a href="#connection-methods"><i class="fas fa-link"></i> Connection Methods</a></li>
123123
<li><a href="#export-methods"><i class="fas fa-file-export"></i> Export Methods</a></li>
124+
<li><a href="#persistence"><i class="fas fa-save"></i> Persistence</a></li>
124125
<li><a href="#examples"><i class="fas fa-code"></i> Examples</a></li>
125126
<li><a href="#algorithms"><i class="fas fa-brain"></i> Algorithms</a></li>
126127
</ul>
@@ -388,6 +389,97 @@ <h4>Parameters</h4>
388389
</div>
389390
</section>
390391

392+
<!-- Persistence Section -->
393+
<section id="persistence" class="api-section">
394+
<h2>Persistence (Save/Load)</h2>
395+
396+
<p>Forests can be saved to and loaded from <code>.forest</code> files, enabling checkpointing during
397+
complex multi-tree growth processes and sharing of generated vascular networks.</p>
398+
399+
<div class="api-method">
400+
<div class="api-method-signature">
401+
<code>save(path, include_timing=False)</code>
402+
</div>
403+
<p>Save the forest to a <code>.forest</code> file.</p>
404+
<div class="api-method-params">
405+
<h4>Parameters</h4>
406+
<ul>
407+
<li><code>path</code> (str): Output filename. If no extension is provided, <code>.forest</code> is appended.</li>
408+
<li><code>include_timing</code> (bool): Include generation timing data for each tree. Default is False.</li>
409+
</ul>
410+
<h4>Returns</h4>
411+
<ul>
412+
<li><code>pathlib.Path</code>: Path to the saved file.</li>
413+
</ul>
414+
<h4>Notes</h4>
415+
<p>The saved file contains all forest data, tree parameters, and connectivity information.
416+
If <code>connect()</code> has been called, connections are also saved. <strong>The domain
417+
is NOT saved</strong> and must be set separately after loading via <code>set_domain()</code>.</p>
418+
</div>
419+
</div>
420+
421+
<div class="api-method">
422+
<div class="api-method-signature">
423+
<code>Forest.load(path)</code> <span class="api-badge">classmethod</span>
424+
</div>
425+
<p>Load a forest from a <code>.forest</code> file.</p>
426+
<div class="api-method-params">
427+
<h4>Parameters</h4>
428+
<ul>
429+
<li><code>path</code> (str): Path to a <code>.forest</code> file.</li>
430+
</ul>
431+
<h4>Returns</h4>
432+
<ul>
433+
<li><code>Forest</code>: Loaded forest instance with all trees and connections restored.</li>
434+
</ul>
435+
<h4>Notes</h4>
436+
<p>The loaded forest will NOT have a domain set. You must call <code>set_domain()</code>
437+
after loading to enable domain-dependent operations. If the forest was saved with
438+
connections (after calling <code>connect()</code>), those connections are restored.</p>
439+
</div>
440+
</div>
441+
442+
<div class="api-example">
443+
<h4>Save/Load Example</h4>
444+
<pre data-copy><code class="language-python">from svv.forest.forest import Forest
445+
from svv.domain.domain import Domain
446+
import pyvista as pv
447+
448+
# Create and grow a forest
449+
domain = Domain(pv.Cube())
450+
domain.create()
451+
domain.solve()
452+
domain.build()
453+
454+
forest = Forest()
455+
forest.set_domain(domain)
456+
forest.set_roots() # Uses default opposing start points
457+
forest.add(50)
458+
forest.connect()
459+
460+
# Save the complete forest (including connections)
461+
forest.save("vascular_network.forest")
462+
463+
# Later, load it back
464+
loaded_forest = Forest.load("vascular_network.forest")
465+
466+
# Re-attach the domain
467+
loaded_forest.set_domain(domain)
468+
469+
# Connections are preserved - can visualize immediately
470+
loaded_forest.show(plot_domain=True)
471+
472+
# Or continue growing
473+
loaded_forest.add(25)</code></pre>
474+
</div>
475+
476+
<div class="callout info">
477+
<strong>Connections Preserved:</strong> Unlike trees, forests can include connection data when
478+
saved. If you saved after calling <code>connect()</code>, the connection geometry and parameters
479+
are restored on load.
480+
</div>
481+
</section>
482+
391483
<!-- Examples Section -->
392484
<section id="examples" class="api-section">
393485
<h2>Examples</h2>

0 commit comments

Comments
 (0)