From 5c21dbbffc4ff966d7f8adf2e48e9da60319d2e5 Mon Sep 17 00:00:00 2001 From: Nejc Jurkovic Date: Fri, 18 Oct 2024 22:57:51 +0200 Subject: [PATCH] Add an automatic high-re grader 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) --- .vscode/settings.json | 2 +- examples/advanced/autograding_highre.py | 7 +- examples/shape/custom.py | 16 +++- .../grading/autograding/grader.py | 53 ++++++++++-- .../grading/autograding/params.py | 81 ++++++++++--------- src/classy_blocks/grading/grading.py | 8 +- 6 files changed, 112 insertions(+), 55 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index f7e11f0..260b324 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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", diff --git a/examples/advanced/autograding_highre.py b/examples/advanced/autograding_highre.py index ce41552..72bc9c9 100644 --- a/examples/advanced/autograding_highre.py +++ b/examples/advanced/autograding_highre.py @@ -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") diff --git a/examples/shape/custom.py b/examples/shape/custom.py index 4b525dd..6c51d8d 100644 --- a/examples/shape/custom.py +++ b/examples/shape/custom.py @@ -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 @@ -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") diff --git a/src/classy_blocks/grading/autograding/grader.py b/src/classy_blocks/grading/autograding/grader.py index e93aa23..4c5ddf1 100644 --- a/src/classy_blocks/grading/autograding/grader.py +++ b/src/classy_blocks/grading/autograding/grader.py @@ -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 @@ -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) @@ -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) @@ -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) diff --git a/src/classy_blocks/grading/autograding/params.py b/src/classy_blocks/grading/autograding/params.py index b380c5f..bf69b53 100644 --- a/src/classy_blocks/grading/autograding/params.py +++ b/src/classy_blocks/grading/autograding/params.py @@ -1,7 +1,7 @@ import abc import dataclasses import warnings -from typing import List, Tuple +from typing import List, Optional, Tuple import scipy.optimize @@ -9,6 +9,8 @@ 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""" @@ -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 @@ -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 @@ -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 diff --git a/src/classy_blocks/grading/grading.py b/src/classy_blocks/grading/grading.py index d8e04b6..28b8b99 100644 --- a/src/classy_blocks/grading/grading.py +++ b/src/classy_blocks/grading/grading.py @@ -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): @@ -96,7 +100,7 @@ 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: @@ -104,7 +108,7 @@ def end_size(self) -> float: 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,