Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b894537
don't refuse to generate output if missing parts. don't crash with e…
ofloveandhate Jun 10, 2026
b8a7f08
Add CLAUDE.md for Claude Code guidance
ofloveandhate Jun 19, 2026
660faef
Expose curve/surface decompositions to Grasshopper via unified-vertex…
ofloveandhate Jun 19, 2026
838d39e
Add standalone script to export a decomposition to Grasshopper JSON
ofloveandhate Jun 19, 2026
623ab32
Add Untangle Curves By Type Grasshopper component
ofloveandhate Jun 19, 2026
efcb3a9
Export the decomposition bounding sphere and expose it in Grasshopper
ofloveandhate Jun 19, 2026
bb6d476
Add Sphere Caps and Close Piece Grasshopper components
ofloveandhate Jun 19, 2026
813c2ba
Add Resolution control to Sphere Caps
ofloveandhate Jun 19, 2026
4fcac25
Fix high-resolution cap holes; add Naked Edges diagnostic to Close Piece
ofloveandhate Jun 19, 2026
acaf3e3
Add Spread Pieces Grasshopper component
ofloveandhate Jun 19, 2026
87f7d05
computing the Hessian of a 3d surface at a singularity reveals the co…
ofloveandhate Jun 19, 2026
ee7b099
Merge remote-tracking branch 'origin/feature/tangent_cones_at_singula…
ofloveandhate Jun 19, 2026
6daced0
Fold singularity/connector data into the unified Grasshopper export
ofloveandhate Jun 19, 2026
4265b8e
Add Rhino-free Python pipeline for capping, closing, and spreading pi…
ofloveandhate Jun 19, 2026
55192f8
Make as_mesh_raw manifold; fix raw-mesh global-vertex alignment
ofloveandhate Jun 19, 2026
1310085
Rewire Surface Place Components to read from Surface Read GH JSON
ofloveandhate Jun 19, 2026
4e90475
Rewire Group By Piece to per-piece trees; drop dead DTOs; unify naming
ofloveandhate Jun 19, 2026
d424d0d
Add PlantUML diagram of the single-source Grasshopper pipeline
ofloveandhate Jun 19, 2026
866cdca
Fix crash in Plug Negative: input index typo (22 -> 2)
ofloveandhate Jun 19, 2026
129b59c
Generalize Spread Pieces to any per-piece geometry tree
ofloveandhate Jun 19, 2026
b308b4c
Warn when Spread Pieces receives a single (flattened) branch
ofloveandhate Jun 19, 2026
c734d28
Add ordered mesh-boolean fold (Boolean Piece) + Connectors To Feature…
ofloveandhate Jun 19, 2026
16d5dbf
Add boolean stage to the Grasshopper usage diagram
ofloveandhate Jun 19, 2026
11bbbc0
Fix Boolean Piece no-op: weld Brep-cutter meshes; warn on open features
ofloveandhate Jun 19, 2026
f3ecc19
Add a Scale input to the readers (import bigger, no Scale component)
ofloveandhate Jun 20, 2026
46dad5c
Boolean Piece: pass Solid through when Features is empty
ofloveandhate Jun 20, 2026
bae4b97
Boolean Piece: fill holes on Brep cutters so open/seamed cylinders close
ofloveandhate Jun 20, 2026
ff9b5be
Remove Size input from Surface Place Components
ofloveandhate Jun 20, 2026
196af96
Place Components: accept any subset of connector geometries
ofloveandhate Jun 20, 2026
7c5a7a9
Add Utility subcategory with a Centered Closed Cylinder component
ofloveandhate Jun 20, 2026
1be12ff
Add Mesh Mode selector component (integer -> mode string)
ofloveandhate Jun 20, 2026
a5e907c
gitattributes: treat .gh as binary, .ghx as text
ofloveandhate Jun 20, 2026
79a9834
Centered Closed Cylinder: add Sides (1 = round, 3+ = N-gon prism)
ofloveandhate Jun 20, 2026
bc3f964
Add Flat Caps component (flat fan to the loop centroid)
ofloveandhate Jun 20, 2026
4fb412d
Add Python twin for flat caps
ofloveandhate Jun 20, 2026
ca73ee4
Add capping/joining tutorial; close_pieces --flat; fix join winding
ofloveandhate Jun 20, 2026
94dc8f4
usage diagram: cross-link the Rhino-free Python tutorial
ofloveandhate Jun 20, 2026
f27add6
Add Color By Function component
ofloveandhate Jun 20, 2026
9236322
Add Spread By Connectors (assembly explosion along connector axes)
ofloveandhate Jun 20, 2026
9e83b8d
Spread Pieces: make Factor an absolute distance (match Spread By Conn…
ofloveandhate Jun 20, 2026
453e294
Rename spread Factor -> Distance (it's an absolute distance, not a mu…
ofloveandhate Jun 20, 2026
49438d3
Spread By Connectors: BFS the connector graph (robust assembly explos…
ofloveandhate Jun 20, 2026
6bfbcb4
usage diagram: add Color By Function, Spread By Connectors, Utility c…
ofloveandhate Jun 20, 2026
5f32301
Sphere Caps / Close Piece: orient cap and solid normals outward
ofloveandhate Jun 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@
# Denote files that are truly binary and should not be modified.
*.png binary
*.jpg binary
*.bmp binary
*.bmp binary

# Grasshopper documents: .gh is binary (no diff/merge); .ghx is XML text (diffable).
*.gh binary
*.ghx text
108 changes: 108 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

Bertini_real is a numerical algorithm for decomposing real algebraic curves (dim 1) and
surfaces (dim 2) into cell complexes, using **Bertini 1** as the homotopy-continuation engine.
The C++ side produces the decomposition; the Python package (`python/`) parses the on-disk
output and renders/plots it. MATLAB code in `matlab_codes/` is legacy post-processing.

Two C++ executables are built: `bertini_real` (computes the decomposition) and `sampler`
(refines/adaptively samples an existing decomposition for smoother output).

## Building (C++)

As of July 2025 the build is **CMake** (previously autotools), and it requires **Bertini 1
version >= 1.7** (which itself moved to CMake — install location and headers changed).

```sh
mkdir build && cd build
cmake ../
make # use -j for parallelism
make install # may need sudo; installs bertini_real + sampler to bin/
```

Dependencies (all must be findable by CMake): MPFR, GMP, `bertini-parallel` (>=1.7, found via
`find_package(bertini1 1.7 CONFIG)`), Boost (>=1.53, components `filesystem` + `timer`), MPI
(openmpi/mpich), plus tools CMake, Flex, Bison. **Bertini 1 must be compiled from source
against the same GMP/MPFR/MPI libraries.** Custom CMake find-modules live in `cmake/`.
`brconfig.h.in` is configured into `build/config.h`. Source-file lists are in `files.cmake`
(not globbed) — **add new `.cpp`/`.hpp` files there**, not just to disk.

Clone recursively (`git clone --recursive`) to get the `matlab_codes/brakelab` submodule.

## Python package

```sh
cd python && pip install -e . # installs the `bertini_real` package
```
Deps: matplotlib, trimesh, dill, algopy, sympy, scipy, networkx. `glumpy` (OpenGL rendering)
is optional and imported defensively in `__init__.py`.

Typical interactive use, run **from inside a decomposition output folder**:
```python
import bertini_real
bertini_real.gather_and_plot() # data.gather() -> plot.plot()
```
`data.gather()` reads the raw on-disk output into a `Curve` or `Surface` object (chosen by the
dimension in the directory name); `data.gather_and_save()` also dills it to a `BRdataN.pkl`.

## Running / tests

There is **no automated test suite or test runner**. `test/curve/*` and `test/surface/*` are
example systems, each a directory containing a Bertini `input` file (and sometimes Python
plot/assemble scripts). The manual workflow for any example:

1. Run Bertini 1 on the `input` file with `tracktype: 1` to produce a numerical irreducible
decomposition and a `witness_data` file.
2. Run `bertini_real` in that directory; it consumes `input` + `witness_data`. If there are
multiple components it prompts for which to decompose. Optional flags:
`-sphere <file> -pi <projectionfile>`.
3. Optionally run `sampler` to refine the decomposition.
4. Use the Python package to plot the result.

Output is plain-text files in a subfolder of cwd, written incrementally after each major stage
(so a crash still leaves the last good parsable state). Key files: `decomp`, `vertex_set`,
copies of `input` + `witness_data`; curves add `E.edge`; surfaces add `S.surf` plus curve
sub-decompositions in their own subfolders. The README and `manual/bertini_real_manual.pdf`
document these formats in full.

## C++ architecture

`src/` and `include/` mirror each other and are organized by subsystem (file lists in
`files.cmake`). `src/bertini_real.cpp` and `src/sampler/` hold the two executable `main`s;
everything else compiles into both (`common_src`).

- **`bertini1/`** — `bertini_extensions`: the C++ bridge to Bertini 1's C structures/headers
(`bertini_headers.hpp`).
- **`nag/`** — numerical algebraic geometry core. `nid` (numerical irreducible decomposition),
`witness_set`, `system_randomizer`, and `nag/solvers/` (the homotopy solvers: `midpoint`,
`multilintolin`, `nullspace`, `sphere_intersection`, `postProcessing`, common `solver`).
- **`decompositions/`** — the top-level algorithms: `curve`, `surface`, base `decomposition`,
and `checkSelfConjugate` (real-vs-complex detection).
- **`cells/`** + **`containers/`** — the cell-complex data model: `vertex`/`edge`/`face`/`cell`,
held in `vertex_set` and `holders`.
- **`symbolics/`** — symbolic preprocessing: `derivative_systems`, `isosingular` (deflation),
`nullspace`, `slicing`, `sphere_intersection`.
- **`io/`** — `fileops`, terminal `color`, and the **Flex** parser `partitionParse.l` (compiled
to `partitionParse.yy.c` at build time; CMake `flex_target` with prefix `partitionParse`).
- top-level: `programConfiguration` (CLI flags / config), `parallelism` (MPI master/worker),
`double_odometer`, `limbo`.

The program is **MPI-parallel** (head/worker model in `parallelism`). C++14.

## Python architecture

`python/bertini_real/` mirrors the C++ cell model in Python objects. `data/` does the parsing
(`gather*`), `curve`/`surface`/`edge`/`face`/`vertex`/`cell`/`decomposition` are the parsed
types, `parse/` reads the directory naming convention, `plot/`/`glumpyplotter`/`anaglypy` do
rendering, `dehomogenize/` handles projective coords, `paths/` and `util/` are helpers.

## Notes

- `documentation/` holds the Doxygen config (`bertini_real.doxy.config`) for the C++ docs at
doc.bertinireal.com/cpp; `python/docs/` is the Sphinx source for doc.bertinireal.com/python.
- GitHub Actions only mirrors pushes to an MPI GitLab (`.github/workflows/github-gitlab-sync.yml`);
there is no CI build/test.
106 changes: 106 additions & 0 deletions grasshopper/bertini_real/Capping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using Rhino;
using Rhino.Geometry;

namespace bertini_real
{
/// <summary>
/// Shared helpers for the cap components (Sphere Caps, Flat Caps): finding the piece mesh's
/// own naked boundary loops that lie on the sphere, and adding non-degenerate triangles.
/// </summary>
internal static class Capping
{
// a tiny absolute distance below which two cap vertices are treated as the same point
public const double CoincidentTol = 1e-9;

/// <summary>
/// Closed loops (ordered lists of TopologyVertex indices) of the mesh's naked boundary that
/// lie on the sphere. Sets <paramref name="unclean"/> if some on-sphere boundary did not
/// form clean degree-2 cycles. Works in topology space (coincident vertices merged); zero-
/// length edges collapse and are ignored.
/// </summary>
public static List<List<int>> OnSphereBoundaryLoops(Mesh mesh, Point3d center, double radius, double tol, out bool unclean)
{
unclean = false;
var result = new List<List<int>>();

var topo = mesh.TopologyVertices;
var edges = mesh.TopologyEdges;

bool OnSphere(int tv)
{
Point3d p = topo[tv];
return Math.Abs(p.DistanceTo(center) - radius) < tol;
}

var adj = new Dictionary<int, List<int>>();
void Link(int u, int v)
{
if (!adj.TryGetValue(u, out var lu)) { lu = new List<int>(); adj[u] = lu; }
if (!lu.Contains(v)) lu.Add(v);
}

for (int e = 0; e < edges.Count; e++)
{
if (edges.GetConnectedFaces(e).Length != 1) continue; // not naked
IndexPair ip = edges.GetTopologyVertices(e);
int i = ip.I, j = ip.J;
if (i == j) continue; // degenerate (collapsed) edge
if (!OnSphere(i) || !OnSphere(j)) continue;
Link(i, j);
Link(j, i);
}

if (adj.Count == 0) return result;

var visited = new HashSet<int>();
foreach (int start in adj.Keys)
{
if (visited.Contains(start)) continue;

var loop = new List<int>();
int prev = -1, cur = start;
bool clean = true;

while (true)
{
visited.Add(cur);
loop.Add(cur);

var nbrs = adj[cur];
if (nbrs.Count != 2) { clean = false; break; } // junction or dead-end

int next = nbrs[0] != prev ? nbrs[0] : nbrs[1];
if (next == start) break; // closed the loop
if (visited.Contains(next)) { clean = false; break; }
prev = cur;
cur = next;
}

if (clean && loop.Count >= 3)
result.Add(loop);
else
unclean = true;
}

return result;
}

/// <summary>
/// Add a triangle, skipping only TRULY degenerate ones (a collapsed edge -- two coincident
/// vertices); thin-but-valid triangles must be kept, or high-resolution caps develop holes.
/// </summary>
public static void AddTri(Mesh m, int i, int j, int k)
{
Point3d a = m.Vertices[i];
Point3d b = m.Vertices[j];
Point3d c = m.Vertices[k];
if (a.DistanceTo(b) < CoincidentTol ||
b.DistanceTo(c) < CoincidentTol ||
a.DistanceTo(c) < CoincidentTol)
return;
m.Faces.AddFace(i, j, k);
}
}
}
99 changes: 99 additions & 0 deletions grasshopper/bertini_real/CurveReadGhJson.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System;
using Grasshopper;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Data;
using Rhino.Geometry;

namespace bertini_real
{
/// <summary>
/// Reads a self-contained standalone-curve export (br_gh_export.json) written by Python's
/// Curve.export_gh_json. Brings the vertices in as ONE unified set and exposes each curve
/// piece as a polyline referring to those vertices by index.
/// </summary>
public class CurveReadGhJson : GH_Component
{
public CurveReadGhJson()
: base("Curve Read GH JSON", "CurveReadJSON",
"Read a bertini_real curve export: one unified vertex set, curve pieces as polylines",
"bertini_real", "Curve")
{
}

protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager)
{
pManager.AddTextParameter("File Path", "F", "Path to br_gh_export.json (a curve export)", GH_ParamAccess.item);
pManager.AddNumberParameter("Scale", "Sc", "Uniform scale applied on import (about the world origin), so the curve comes in bigger without a Scale component", GH_ParamAccess.item, 1.0);
Params.Input[1].Optional = true;
}

protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager)
{
pManager.AddPointParameter("Vertices", "V", "The single unified vertex set; list index = global vertex id", GH_ParamAccess.list);
pManager.AddCurveParameter("Curves", "C", "One polyline per curve piece", GH_ParamAccess.tree);
pManager.AddTextParameter("Curve Types", "T", "Type tag per curve piece, parallel to Curves", GH_ParamAccess.tree);
pManager.AddIntegerParameter("Curve Indices", "CI", "Per curve piece: vertex indices into Vertices", GH_ParamAccess.tree);
}

protected override void SolveInstance(IGH_DataAccess DA)
{
string path = "";
if (!DA.GetData(0, ref path)) return;

double scale = 1.0;
DA.GetData(1, ref scale);

GhExport content;
try
{
content = GhJsonIO.Load(path);
}
catch (Exception e)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Failed to read/parse JSON: " + e.Message);
return;
}

if (content == null || content.decomposition_type != "curve")
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Not a curve export (decomposition_type != 'curve').");
return;
}

var verts = GhJsonIO.ToVertices(content);
if (scale != 1.0)
for (int i = 0; i < verts.Count; i++)
verts[i] = verts[i] * scale;

var curves = new DataTree<Curve>();
var types = new DataTree<string>();
var curveIdx = new DataTree<int>();

if (content.curve_pieces != null)
{
foreach (var cp in content.curve_pieces)
{
var branch = new GH_Path(cp.piece_index);

PolylineCurve pl = GhJsonIO.ToPolyline(cp.vertex_indices, verts);
if (pl != null) curves.Add(pl, branch);

types.Add(cp.type, branch);
if (cp.vertex_indices != null) curveIdx.AddRange(cp.vertex_indices, branch);
}
}

DA.SetDataList(0, verts);
DA.SetDataTree(1, curves);
DA.SetDataTree(2, types);
DA.SetDataTree(3, curveIdx);
}

protected override System.Drawing.Bitmap Icon => IconLoader.GetIcon("telephone.png");

public override Guid ComponentGuid
{
get { return new Guid("7C2A9F10-3B8E-4D55-A1C7-6E0B4F92D38B"); }
}
}
}
Loading