Skip to content

Commit

Permalink
Add an automatic high-re grader
Browse files Browse the repository at this point in the history
A very first attempt, much left to polish.

Sets cell count first, copies it, then adjusts edge gradings so that
transitions between blocks are as smooth as possible.

Fix a bug in Grading (start_size/end_size)

Set explicit import sorting (deleted/incorrectly sorted stuff on save)
  • Loading branch information
FranzBangar committed Oct 18, 2024
1 parent 4f35337 commit 5c21dbb
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 55 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.wordBasedSuggestions": "off",
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
"source.organizeImports.ruff": "explicit"
},
},
"python.analysis.typeCheckingMode": "basic",
Expand Down
7 changes: 4 additions & 3 deletions examples/advanced/autograding_highre.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@

mesh.set_default_patch("walls", "wall")

params = HighReChopParams(0.1)

params = HighReChopParams(0.075)
grader = HighReGrader(mesh, params)
grader.grade(take="max")

params = SimpleChopParams(0.1)
params = SimpleChopParams(0.075)
grader = SimpleGrader(mesh, params)
# grader.grade()
# grader.grade(take="max")

mesh.write(os.path.join("..", "case", "system", "blockMeshDict"), debug_path="debug.vtk")
16 changes: 12 additions & 4 deletions examples/shape/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import numpy as np

import classy_blocks as cb
from classy_blocks.grading.autograding.grader import HighReGrader
from classy_blocks.grading.autograding.params import HighReChopParams
from classy_blocks.types import PointType
from classy_blocks.util import functions as f

Expand Down Expand Up @@ -61,10 +63,16 @@ def add_edges(self) -> None:

shape = cb.ExtrudedShape(base_1, 1)

for op in shape.operations:
for i in range(3):
op.chop(i, count=10)

# for op in shape.operations:
# for i in range(3):
# op.chop(i, count=10)
mesh.add(shape)
mesh.assemble()
mesh.block_list.update()

params = HighReChopParams(0.03)
grader = HighReGrader(mesh, params)
grader.grade(take="max")

mesh.set_default_patch("walls", "wall")
mesh.write(os.path.join("..", "case", "system", "blockMeshDict"), debug_path="debug.vtk")
53 changes: 45 additions & 8 deletions src/classy_blocks/grading/autograding/grader.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Set, get_args
from typing import Optional, Set, get_args

from classy_blocks.grading.autograding.params import ChopParams, FixedCountParams, HighReChopParams, SimpleChopParams
from classy_blocks.grading.autograding.probe import Probe
from classy_blocks.grading.chop import Chop
from classy_blocks.items.wires.wire import Wire
from classy_blocks.mesh import Mesh
from classy_blocks.types import ChopTakeType, DirectionType
Expand All @@ -15,17 +16,17 @@ def __init__(self, mesh: Mesh, params: ChopParams):
self.mesh.assemble()
self.probe = Probe(self.mesh)

def _get_end_size(self, wires: Set[Wire]):
def _get_end_size(self, wires: Set[Wire]) -> Optional[float]:
"""Returns average size of wires' last cell"""
if len(wires) == 0:
return 0
return None

return sum(wire.grading.end_size for wire in wires) / len(wires)

def _get_start_size(self, wires: Set[Wire]):
def _get_start_size(self, wires: Set[Wire]) -> Optional[float]:
"""Returns average size of wires' first cell"""
if len(wires) == 0:
return 0
return None

return sum(wire.grading.start_size for wire in wires) / len(wires)

Expand All @@ -42,18 +43,21 @@ def grade_axis(self, axis: DirectionType, take: ChopTakeType) -> None:
else:
# take length from a row, as requested
length = row.get_length(take)
# and set count from it
count = self.params.get_count(length)

for wire in row.get_wires():
# don't touch defined wires
if wire.is_defined:
# TODO: test
continue
# TODO! don't touch wires, defined by USER
# if wire.is_defined:
# # TODO: test
# continue

size_before = self._get_end_size(wire.before)
size_after = self._get_start_size(wire.after)
chops = self.params.get_chops(count, wire.length, size_before, size_after)

wire.grading.clear()
for chop in chops:
wire.grading.add_chop(chop)

Expand Down Expand Up @@ -87,3 +91,36 @@ class HighReGrader(GraderBase):

def __init__(self, mesh: Mesh, params: HighReChopParams):
super().__init__(mesh, params)

def grade_axis(self, axis, take) -> None:
for row in self.probe.get_rows(axis):
# determine count
wires = row.get_wires()

for wire in reversed(wires):
if wire.is_defined:
# there's a wire with a defined count already, use that
count = wire.grading.count
break
else:
# take length from a row, as requested
length = row.get_length(take)
# and set count from it
count = self.params.get_count(length)

for wire in row.get_wires():
# don't touch defined wires
# TODO! don't touch wires, defined by USER
# if wire.is_defined:
# # TODO: test
# continue

# make a rudimentary chop first, then adjust
# in subsequent passes
chops = [Chop(length_ratio=0.5, count=count // 2), Chop(length_ratio=0.5, count=count // 2)]

for chop in chops:
wire.grading.add_chop(chop)

super().grade_axis(axis, take)
super().grade_axis(axis, take)
81 changes: 44 additions & 37 deletions src/classy_blocks/grading/autograding/params.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import abc
import dataclasses
import warnings
from typing import List, Tuple
from typing import List, Optional, Tuple

import scipy.optimize

import classy_blocks.grading.relations as gr
from classy_blocks.grading.chop import Chop
from classy_blocks.types import ChopTakeType

CellSizeType = Optional[float]


def sum_length(start_size: float, count: int, c2c_expansion: float) -> float:
"""Returns absolute length of the chop"""
Expand All @@ -28,7 +30,7 @@ def get_count(self, length: float) -> int:
"""Calculates count based on given length - used once only"""

@abc.abstractmethod
def get_chops(self, count: int, length: float, size_before: float = 0, size_after: float = 0) -> List[Chop]:
def get_chops(self, count: int, length: float, size_before: CellSizeType, size_after: CellSizeType) -> List[Chop]:
"""Fixes cell count but modifies chops so that proper cell sizing will be obeyed"""
# That depends on inherited classes' philosophy

Expand All @@ -44,7 +46,6 @@ def get_chops(self, count, _length, _size_before=0, _size_after=0) -> List[Chop]
return [Chop(count=count)]


# TODO: rename this CentipedeCaseClassNameMonstrosity
@dataclasses.dataclass
class SimpleChopParams(ChopParams):
cell_size: float
Expand All @@ -69,50 +70,56 @@ def get_count(self, length: float):

return count

def get_chops(self, count, length, size_before=0, size_after=0):
# length of the wire that was used to set count
if size_before == 0:
size_before = self.cell_size
if size_after == 0:
size_after = self.cell_size
def define_sizes(
self, count: int, length: float, size_before: CellSizeType, size_after: CellSizeType
) -> Tuple[float, float]:
"""Defines start and end cell size with respect to given circumstances"""
if size_before == 0 or size_after == 0:
# until all counts/sizes are defined
# (the first pass with uniform grading),
# there's no point in doing anything
raise RuntimeError("Undefined grading encountered!")

chops = [
Chop(count=count // 2),
Chop(count=count // 2),
]
# not enough room for all cells?
cramped = self.cell_size * count > length

def objfun(params):
chops[0].length_ratio = params[0]
chops[1].length_ratio = 1 - params[0]
if size_before is None:
if cramped:
size_before = length / count
else:
size_before = self.cell_size

chops[0].total_expansion = params[1]
chops[1].total_expansion = params[2]
if size_after is None:
if cramped:
size_after = length / count
else:
size_after = self.cell_size

data_1 = chops[0].calculate(length)
data_2 = chops[1].calculate(length)
return size_before, size_after

ofstart = (size_before - data_1.start_size) ** 2
ofmid1 = (data_1.end_size - self.cell_size) ** 2
ofmid2 = (data_2.start_size - self.cell_size) ** 2
ofend = (data_2.end_size - size_after) ** 2
def get_chops(self, count, length, size_before=CellSizeType, size_after=CellSizeType):
size_before, size_after = self.define_sizes(count, length, size_before, size_after)

return max([ofstart, ofmid1, ofmid2, ofend])
# choose length ratio so that cells at the middle of blocks
# (between the two chops) have the same size
def fobj(lratio):
halfcount = count // 2
chop_1 = Chop(length_ratio=lratio, count=halfcount, start_size=size_before)
data_1 = chop_1.calculate(length)

initial = [0.5, 1, 1]
bounds = (
(0.1, 0.9),
(0.1, 10),
(0.1, 10),
)
result = scipy.optimize.minimize(objfun, initial, bounds=bounds).x
chop_2 = Chop(length_ratio=1 - lratio, count=halfcount, end_size=size_after)
data_2 = chop_2.calculate(length)

ratio = abs(data_1.end_size - data_2.start_size)

chops[0].length_ratio = result[0]
chops[1].length_ratio = 1 - result[0]
return ratio, [chop_1, chop_2]

chops[0].total_expansion = result[1]
chops[1].total_expansion = result[2]
# it's not terribly important to minimize until the last dx
results = scipy.optimize.minimize_scalar(lambda r: fobj(r)[0], bounds=[0.1, 0.9], options={"xatol": 0.1})
if not results.success: # type:ignore
warnings.warn("Could not determine optimal grading", stacklevel=1)

return chops
return fobj(results.x)[1] # type:ignore


# INVALID! Next on list
Expand Down
8 changes: 6 additions & 2 deletions src/classy_blocks/grading/grading.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ def add_chop(self, chop: Chop) -> None:

self.chops.append(chop)

def clear(self) -> None:
self.chops = []
self._chop_data = []

@property
def chop_data(self) -> List[ChopData]:
if len(self._chop_data) < len(self.chops):
Expand Down Expand Up @@ -96,15 +100,15 @@ def start_size(self) -> float:
return 0

chop = self.chops[0]
return chop.calculate(self.length * chop.length_ratio).start_size
return chop.calculate(self.length).start_size

@property
def end_size(self) -> float:
if len(self.chops) == 0:
return 0

chop = self.chops[-1]
return chop.calculate(self.length * chop.length_ratio).end_size
return chop.calculate(self.length).end_size

def copy(self, length: float, invert: bool = False) -> "Grading":
"""Creates a new grading with the same chops (counts) on a different length,
Expand Down

0 comments on commit 5c21dbb

Please sign in to comment.