Skip to content

Commit

Permalink
Merge branch 'feature/stacks' of github.com:damogranlabs/classy_block…
Browse files Browse the repository at this point in the history
…s into feature/stacks
  • Loading branch information
FranzBangar committed May 24, 2024
2 parents 2fcedb1 + 3d7be7f commit bc4195c
Show file tree
Hide file tree
Showing 15 changed files with 452 additions and 210 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Check out the [classy_blocks tutorial on damogranlabs.com](https://damogranlabs.

# How To Use It
- To install the current _stable_ version from pypi, use `pip install classy_blocks`
- To download the cutting-edge development version, unstall the development branch from github: `pip install git+https://github.com/damogranlabs/classy_blocks.git@development`
- To download the cutting-edge development version, install the development branch from github: `pip install git+https://github.com/damogranlabs/classy_blocks.git@development`
- If you want to run examples, follow instructions in [Examples](#examples)
- If you want to contribute, follow instructions in [CONTRIBUTING.rst](CONTRIBUTING.rst)

Expand Down
22 changes: 8 additions & 14 deletions examples/complex/heater/heater.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@

import classy_blocks as cb
from classy_blocks.construct.flat.sketch import Sketch
from classy_blocks.construct.flat.sketches.disk import WrappedDisk
from classy_blocks.construct.operations.operation import Operation

# TODO: direct imports!
from classy_blocks.construct.shape import ExtrudedShape, Shape
from classy_blocks.construct.stack import RevolvedStack

mesh = cb.Mesh()

Expand All @@ -21,7 +15,7 @@
zlv = p.zlv


def set_cell_zones(shape: Shape):
def set_cell_zones(shape: cb.Shape):
solid_indexes = list(range(5))
fluid_indexes = list(range(5, 9))

Expand All @@ -33,11 +27,11 @@ def set_cell_zones(shape: Shape):


# Cross-section of heater and fluid around it
WrappedDisk.chops[0] = [1] # the solid part, chop the fluid zone manually
heater_xs = WrappedDisk(p.heater_start_point, p.wrapping_corner_point, p.heater_diameter / 2, [1, 0, 0])
cb.WrappedDisk.chops[0] = [1] # the solid part, chop the fluid zone manually
heater_xs = cb.WrappedDisk(p.heater_start_point, p.wrapping_corner_point, p.heater_diameter / 2, [1, 0, 0])

# The straight part of the heater, part 1: bottom
straight_1 = ExtrudedShape(heater_xs, p.heater_length)
straight_1 = cb.ExtrudedShape(heater_xs, p.heater_length)

straight_1.chop(0, start_size=p.solid_cell_size)
straight_1.chop(1, start_size=p.solid_cell_size)
Expand All @@ -48,15 +42,15 @@ def set_cell_zones(shape: Shape):


# The curved part of heater (and fluid around it); constructed from 4 revolves
heater_arch = RevolvedStack(straight_1.sketch_2, np.pi, [0, 0, 1], [0, 0, 0], 4)
heater_arch = cb.RevolvedStack(straight_1.sketch_2, np.pi, [0, 0, 1], [0, 0, 0], 4)
heater_arch.chop(start_size=p.solid_cell_size, take="min")
for shape in heater_arch.shapes:
set_cell_zones(shape)
mesh.add(heater_arch)


# The straight part of heater, part 2: after the arch
straight_2 = ExtrudedShape(heater_arch.shapes[-1].sketch_2, p.heater_length)
straight_2 = cb.ExtrudedShape(heater_arch.shapes[-1].sketch_2, p.heater_length)
set_cell_zones(straight_2)
mesh.add(straight_2)

Expand All @@ -70,7 +64,7 @@ def set_cell_zones(shape: Shape):
# A custom sketch that takes the closest faces from given operations;
# They will definitely be wrongly oriented but we'll sort that out later
class NearestSketch(Sketch):
def __init__(self, operations: List[Operation], far_point):
def __init__(self, operations: List[cb.Operation], far_point):
far_point = np.array(far_point)
self._faces = [op.get_closest_face(far_point) for op in operations]

Expand All @@ -88,7 +82,7 @@ def center(self):


cylinder_xs = NearestSketch([arch_fill.operations[i] for i in (0, 1, 2, 5)], [-2 * p.heater_length, 0, 0])
pipe_fill = ExtrudedShape(cylinder_xs, [-p.heater_length, 0, 0])
pipe_fill = cb.ExtrudedShape(cylinder_xs, [-p.heater_length, 0, 0])

# reorient the operations in the shape
reorienter = cb.ViewpointReorienter([-2 * p.heater_length, 0, 0], [0, p.heater_length, 0])
Expand Down
7 changes: 7 additions & 0 deletions src/classy_blocks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
from .construct.operations.connector import Connector
from .construct.operations.extrude import Extrude
from .construct.operations.loft import Loft
from .construct.operations.operation import Operation
from .construct.operations.revolve import Revolve
from .construct.operations.wedge import Wedge
from .construct.shape import ExtrudedShape, LoftedShape, RevolvedShape, Shape
from .construct.shapes.cylinder import Cylinder, SemiCylinder
from .construct.shapes.elbow import Elbow
from .construct.shapes.frustum import Frustum
Expand Down Expand Up @@ -52,6 +54,7 @@
"OnCurve",
"Face",
# construct operations
"Operation",
"Loft",
"Extrude",
"Revolve",
Expand All @@ -67,6 +70,10 @@
"WrappedDisk",
"Oval",
# construct shapes
"Shape",
"ExtrudedShape",
"LoftedShape",
"RevolvedShape",
"Elbow",
"Frustum",
"Cylinder",
Expand Down
114 changes: 114 additions & 0 deletions src/classy_blocks/construct/flat/map.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from typing import Dict, List, Set

import numpy as np
import scipy.optimize

from classy_blocks.construct.flat.quad import Quad
from classy_blocks.types import NPPointListType, QuadIndexType
from classy_blocks.util import functions as f


class QuadMap:
def __init__(self, positions: NPPointListType, indexes: List[QuadIndexType]):
self.indexes = indexes
self.positions = positions

self.quads = [Quad(self.positions, quad_indexes) for quad_indexes in indexes]

def update(self) -> None:
for quad in self.quads:
quad.update()

@property
def connections(self) -> List[Set[int]]:
return f.flatten_2d_list([quad.connections for quad in self.quads])

@property
def neighbours(self) -> Dict[int, Set[int]]:
"""Returns a dictionary point:[neighbour points] as defined by quads"""
length = int(max(np.array(self.indexes).ravel())) + 1

neighbours: Dict[int, Set[int]] = {i: set() for i in range(length)}
connections = self.connections

for connection in connections:
clist = list(connection)
neighbours[clist[0]].add(clist[1])
neighbours[clist[1]].add(clist[0])

return neighbours

@property
def fixed_points(self) -> Set[int]:
"""Returns indexes of points that can be smoothed"""
connections = self.connections
fixed_points: Set[int] = set()

for edge in connections:
if connections.count(edge) == 1:
fixed_points.update(edge)

return fixed_points

def smooth_laplacian(self, iterations: int = 5) -> None:
"""Smooth the points using laplacian smoothing;
each point is moved to the average of its neighbours"""
neighbours = self.neighbours
fixed_points = self.fixed_points

for _ in range(iterations):
for point_index, point_neighbours in neighbours.items():
if point_index in fixed_points:
continue

nei_positions = [self.positions[i] for i in point_neighbours]

self.positions[point_index] = np.average(nei_positions, axis=0)

self.update()

def get_nearby_quads(self) -> Dict[int, List[Quad]]:
"""Returns a list of quads that contain each movable point"""
corner_points: Dict[int, List[Quad]] = {}

fixed_points = self.fixed_points

for i, point in enumerate(self.positions):
if i in fixed_points:
continue

corner_points[i] = []

for quad in self.quads:
if quad.contains(point):
corner_points[i].append(quad)

return corner_points

def optimize_energy(self):
"""Replace quad edges by springs and moves their positions
so that all springs are in the most relaxed state possible,
minimizing the energy of the system"""
# get quads that are defined by each point
fixed_points = self.fixed_points

e1 = self.quads[0].e1
e2 = self.quads[0].e2

def energy(i, x) -> float:
e = 0

self.positions[i] = x[0] * e1 + x[1] * e2

for quad in self.quads:
quad.update()
e += quad.energy

return e

for i in range(len(self.positions)):
if i in fixed_points:
continue

initial = [1, 1]
scipy.optimize.minimize(lambda x, i=i: energy(i, x), initial)
90 changes: 46 additions & 44 deletions src/classy_blocks/construct/flat/quad.py
Original file line number Diff line number Diff line change
@@ -1,69 +1,71 @@
from typing import Dict, List, Set, Tuple
from typing import List, Set

import numpy as np

from classy_blocks.construct.flat.face import Face
from classy_blocks.types import NPPointListType
from classy_blocks.types import NPPointListType, NPPointType, QuadIndexType
from classy_blocks.util import functions as f
from classy_blocks.util.constants import DTYPE

QuadType = Tuple[int, int, int, int]
from classy_blocks.util.constants import TOL


class Quad:
"""A helper class for tracking positions-faces-indexes-neighbours-whatnot"""

def __init__(self, positions: NPPointListType, indexes: Tuple[int, int, int, int]):
def __init__(self, positions: NPPointListType, indexes: QuadIndexType):
self.indexes = indexes
self.face = Face(np.take(positions, list(indexes), axis=0))


# TODO: move functions into Quad or remove Quad at all
def get_connections(quad: QuadType) -> List[Set[int]]:
return [{quad[i], quad[(i + 1) % 4]} for i in range(4)]


def get_all_connections(quads) -> List[Set[int]]:
return f.flatten_2d_list([get_connections(quad) for quad in quads])
self.positions = positions
self.face = Face([self.positions[i] for i in self.indexes])

def update(self) -> None:
"""Update Face position"""
self.face.update([self.positions[i] for i in self.indexes])

def find_neighbours(quads: List[QuadType]) -> Dict[int, Set[int]]:
"""Returns a dictionary point:[neighbour points] as defined by quads"""
length = int(max(np.array(quads, dtype=DTYPE).ravel())) + 1
def contains(self, point: NPPointType) -> bool:
"""Returns True if the given point is a part of this quad"""
for this_point in self.points:
if f.norm(point - this_point) < TOL:
return True

neighbours: Dict[int, Set[int]] = {i: set() for i in range(length)}
connections = get_all_connections(quads)
return False

for connection in connections:
clist = list(connection)
neighbours[clist[0]].add(clist[1])
neighbours[clist[1]].add(clist[0])
@property
def points(self) -> NPPointListType:
return self.face.point_array

return neighbours
@property
def connections(self) -> List[Set[int]]:
return [{self.indexes[i], self.indexes[(i + 1) % 4]} for i in range(4)]

@property
def perimeter(self):
return sum([f.norm(self.points[i] - self.points[(i + 1) % 4]) for i in range(4)])

def get_fixed_points(quads) -> Set[int]:
"""Returns indexes of points that can be smoothed"""
connections = get_all_connections(quads)
fixed_points: Set[int] = set()
@property
def center(self):
return np.average(self.points, axis=0)

for edge in connections:
if connections.count(edge) == 1:
fixed_points.update(edge)
@property
def energy(self):
e = 0

return fixed_points
ideal_side = self.perimeter / 4
ideal_diagonal = (ideal_side / 2) * 2**0.5
center = self.center

for i in range(4):
e += (f.norm(self.points[i] - self.points[(i + 1) % 4]) - ideal_side) ** 2
e += (f.norm(center - self.points[i]) - ideal_diagonal) ** 2

def smooth(positions, quads, iterations: int) -> NPPointListType:
neighbours = find_neighbours(quads)
fixed_points = get_fixed_points(quads)
return e / 8

for _ in range(iterations):
for point_index, point_neighbours in neighbours.items():
if point_index in fixed_points:
continue
@property
def e1(self):
return f.unit_vector(self.points[1] - self.points[0])

nei_positions = np.take(positions, list(point_neighbours), axis=0)
positions[point_index] = np.average(nei_positions, axis=0)
@property
def normal(self):
return f.unit_vector(np.cross(self.points[1] - self.points[0], self.points[3] - self.points[0]))

return positions
@property
def e2(self):
return f.unit_vector(-np.cross(self.e1, self.normal))
Loading

0 comments on commit bc4195c

Please sign in to comment.