Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8124f1d
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Dec 10, 2025
68210b2
Simplify serialization logic to be dependent on only the file_state, …
wli51 Nov 20, 2025
2a48af1
Add crop dataset class for handling image crops, also introduce the s…
wli51 Nov 20, 2025
adaed09
Removing redundant method overrides
wli51 Nov 20, 2025
132b6c6
Remove unused imports from crop_dataset.py
wli51 Nov 22, 2025
d770f89
Merge branch 'dev-add-patch-dataset' of https://github.com/wli51/virt…
wli51 Dec 10, 2025
678c592
Merge branch 'dev-add-patch-dataset'
wli51 Dec 10, 2025
59e0c53
Restore Tuple import in crop_manifest from dev-add-patch-dataset
wli51 Dec 10, 2025
91c3e1d
Merge remote-tracking branch 'upstream/main'
wli51 Jul 22, 2026
aeed0c3
Back track unit tests for Conv2DConvNeXtBlock and Conv2DNormActBlock
wli51 Jul 22, 2026
e849837
Back track unit tests contracting up/down block output channel and sp…
wli51 Jul 22, 2026
2dd6e81
Reorganize blocks as a subpackage to make future expansion convenient…
wli51 Jul 22, 2026
2400e44
add compatibility shim to prevent breaking of examples
wli51 Jul 24, 2026
4090c15
Add up_down_blocks.py with compatibility shim for import behavior
wli51 Jul 24, 2026
f203370
Fix import of AbstractBlock to improve module organization
wli51 Jul 24, 2026
33ac6f5
Refactor modular UNet example to use CropImageDataset and BaseImageDa…
wli51 Jul 24, 2026
7238a7d
Backtrack patch based discriminator update for faithful pix2pix wgan …
wli51 Jul 24, 2026
4c57cb7
Backtrack example training and augmentation and reflect an update in …
wli51 Jul 25, 2026
be86c03
Backtrack unit tests for PatchBasedDiscriminator functionality and co…
wli51 Jul 25, 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
34 changes: 34 additions & 0 deletions src/virtual_stain_flow/models/blocks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from .blocks import (
AbstractBlock,
Conv2DConvNeXtBlock,
Conv2DNormActBlock,
)
from .up_down_blocks import (
Bilinear2DUpsampleBlock,
Conv2DDownBlock,
ConvTrans2DUpBlock,
IdentityBlock,
MaxPool2DDownBlock,
PixelShuffle2DUpBlock,
)
from .utils import (
ActivationType,
get_activation,
get_norm
)


__all__ = [
"AbstractBlock",
"Conv2DConvNeXtBlock",
"Conv2DNormActBlock",
"Bilinear2DUpsampleBlock",
"Conv2DDownBlock",
"ConvTrans2DUpBlock",
"IdentityBlock",
"MaxPool2DDownBlock",
"PixelShuffle2DUpBlock",
"ActivationType",
"get_activation",
"get_norm",
]
Original file line number Diff line number Diff line change
Expand Up @@ -51,23 +51,19 @@ class AbstractDownBlock(AbstractBlock):
def __init__(self, in_channels, out_channels, num_units, **kwargs):
super().__init__(in_channels, out_channels, num_units, **kwargs)

@property
def out_h(self, in_h: int) -> int:
return in_h // 2

@property
def out_w(self, in_w: int) -> int:
return in_w // 2

class AbstractUpBlock(AbstractBlock):
def __init__(self, in_channels, out_channels, num_units, **kwargs):
super().__init__(in_channels, out_channels, num_units, **kwargs)

@property
def out_h(self, in_h: int) -> int:
return in_h * 2

@property
def out_w(self, in_w: int) -> int:
return in_w * 2

Expand Down
2 changes: 1 addition & 1 deletion src/virtual_stain_flow/models/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

from .encoder import Encoder
from .decoder import Decoder
from .utils import (
from .blocks import (
get_activation,
ActivationType
)
Expand Down
2 changes: 1 addition & 1 deletion src/virtual_stain_flow/models/stages.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
AbstractBlock,
Conv2DNormActBlock
)
from .up_down_blocks import (
from .blocks import (
Conv2DDownBlock,
ConvTrans2DUpBlock
)
Expand Down
8 changes: 4 additions & 4 deletions src/virtual_stain_flow/models/unet.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@
import torch
import torch.nn as nn

from .utils import ActivationType
from .factory import qualname
from .model import BaseGeneratorModel
from .encoder import Encoder
from .decoder import Decoder
from .blocks import Conv2DNormActBlock
from .up_down_blocks import (
from .blocks import (
Conv2DNormActBlock,
IdentityBlock,
Conv2DDownBlock,
MaxPool2DDownBlock,
ConvTrans2DUpBlock,
Bilinear2DUpsampleBlock
Bilinear2DUpsampleBlock,
ActivationType
)

class UNet(BaseGeneratorModel):
Expand Down
9 changes: 5 additions & 4 deletions src/virtual_stain_flow/models/unext.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
import torch
import torch.nn as nn

from .utils import ActivationType
from .factory import qualname
from .model import BaseGeneratorModel
from .decoder import Decoder
from .blocks import Conv2DConvNeXtBlock, Conv2DNormActBlock
from .up_down_blocks import (
from .blocks import (
Conv2DConvNeXtBlock,
Conv2DNormActBlock,
ConvTrans2DUpBlock,
PixelShuffle2DUpBlock
PixelShuffle2DUpBlock,
ActivationType
)

class ConvNeXtUNet(BaseGeneratorModel):
Expand Down
176 changes: 176 additions & 0 deletions tests/models/test_compute_blocks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import timm
import torch
import pytest
import torch.nn as nn

from virtual_stain_flow.models.blocks import (
AbstractBlock,
Conv2DConvNeXtBlock,
Conv2DNormActBlock,
)


class DummyBlock(AbstractBlock):
def forward(self, x):
return x


class TestAbstractBlock:
@pytest.mark.parametrize(
("parameter", "value", "exception", "message"),
[
("in_channels", "3", TypeError, "Expected in_channels to be int"),
("in_channels", 0, ValueError, "Expected in_channels to be positive"),
("in_channels", -1, ValueError, "Expected in_channels to be positive"),
("out_channels", "8", TypeError, "Expected out_channels to be int"),
("out_channels", 0, ValueError, "Expected out_channels to be positive"),
("out_channels", -1, ValueError, "Expected out_channels to be positive"),
("num_units", "2", TypeError, "Expected num_units to be int"),
("num_units", 0, ValueError, "Expected num_units to be positive"),
("num_units", -1, ValueError, "Expected num_units to be positive"),
],
)
def test_rejects_invalid_channel_and_unit_values(
self, parameter, value, exception, message
):
kwargs = {"in_channels": 3, "out_channels": 8, "num_units": 2}
kwargs[parameter] = value

with pytest.raises(exception, match=message):
DummyBlock(**kwargs)


class TestConv2DConvNeXtBlock:
@pytest.mark.parametrize(
("in_channels", "out_channels"),
[(1, 2), (3, 8), (8, 3)],
)
def test_convnext_block_produces_configured_output_channels(
self, in_channels, out_channels
):
block = Conv2DConvNeXtBlock(
in_channels=in_channels, out_channels=out_channels
)

output = block(torch.randn(2, in_channels, 16, 16))

assert output.shape == (2, out_channels, 16, 16)

def test_convnext_block_defaults_output_channels_to_input_channels(self):
block = Conv2DConvNeXtBlock(in_channels=3)

output = block(torch.randn(2, 3, 16, 16))

assert output.shape == (2, 3, 16, 16)

@pytest.mark.parametrize("num_units", [1, 2, 3])
def test_convnext_block_appends_configured_number_of_units(self, num_units):
block = Conv2DConvNeXtBlock(
in_channels=3, out_channels=8, num_units=num_units
)

convnext_units = [
layer
for layer in block.network
if isinstance(layer, timm.models.convnext.ConvNeXtBlock)
]

assert len(convnext_units) == num_units

def test_convnext_block_exposes_abstract_block_metadata_and_dimensions(self):
input_tensor = torch.randn(2, 3, 12, 18)
block = Conv2DConvNeXtBlock(in_channels=3, out_channels=8, num_units=2)

output = block(input_tensor)

assert block.in_channels == input_tensor.shape[1]
assert block.out_channels == output.shape[1]
assert block.num_units == 2
assert block.out_h(input_tensor.shape[2]) == output.shape[2]
assert block.out_w(input_tensor.shape[3]) == output.shape[3]


class TestConv2DNormActBlock:
@pytest.mark.parametrize(
("in_channels", "out_channels"),
[(1, 2), (3, 8), (8, 3)],
)
def test_norm_act_block_produces_configured_output_channels(
self, in_channels, out_channels
):
block = Conv2DNormActBlock(
in_channels=in_channels, out_channels=out_channels
)

output = block(torch.randn(2, in_channels, 16, 16))

assert output.shape == (2, out_channels, 16, 16)

def test_norm_act_block_defaults_output_channels_to_input_channels(self):
block = Conv2DNormActBlock(in_channels=3)

output = block(torch.randn(2, 3, 16, 16))

assert output.shape == (2, 3, 16, 16)

@pytest.mark.parametrize("num_units", [1, 2, 3])
def test_norm_act_block_appends_configured_number_of_convolutions(
self, num_units
):
block = Conv2DNormActBlock(
in_channels=3, out_channels=8, num_units=num_units
)

convolutions = [
layer for layer in block.network if isinstance(layer, nn.Conv2d)
]

assert len(convolutions) == num_units

@pytest.mark.parametrize(
("norm_type", "expected_type"),
[
("batch", nn.BatchNorm2d),
("layer", nn.GroupNorm),
("none", nn.Identity),
],
)
def test_norm_act_block_uses_configured_normalization(
self, norm_type, expected_type
):
block = Conv2DNormActBlock(in_channels=3, norm_type=norm_type)

normalization_layers = block.network[1::3]

assert all(isinstance(layer, expected_type) for layer in normalization_layers)

@pytest.mark.parametrize(
("act_type", "expected_type"),
[
("relu", nn.ReLU),
("gelu", nn.GELU),
("sigmoid", nn.Sigmoid),
("softmax", nn.Softmax),
("none", nn.Identity),
],
)
def test_norm_act_block_uses_configured_activation(
self, act_type, expected_type
):
block = Conv2DNormActBlock(in_channels=3, act_type=act_type)

activation_layers = block.network[2::3]

assert all(isinstance(layer, expected_type) for layer in activation_layers)

def test_norm_act_block_exposes_abstract_block_metadata_and_dimensions(self):
input_tensor = torch.randn(2, 3, 12, 18)
block = Conv2DNormActBlock(in_channels=3, out_channels=8, num_units=2)

output = block(input_tensor)

assert block.in_channels == input_tensor.shape[1]
assert block.out_channels == output.shape[1]
assert block.num_units == 2
assert block.out_h(input_tensor.shape[2]) == output.shape[2]
assert block.out_w(input_tensor.shape[3]) == output.shape[3]
50 changes: 50 additions & 0 deletions tests/models/test_up_down_blocks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import pytest
import torch

from virtual_stain_flow.models.blocks import (
Bilinear2DUpsampleBlock,
Conv2DDownBlock,
ConvTrans2DUpBlock,
IdentityBlock,
MaxPool2DDownBlock,
PixelShuffle2DUpBlock,
)


class TestUpDownBlocks:
@pytest.mark.parametrize(
("block_type", "kwargs", "in_channels", "expected_out_channels", "scale"),
[
(IdentityBlock, {}, 3, 3, 1),
(IdentityBlock, {"out_channels": 8}, 3, 3, 1),
(Conv2DDownBlock, {}, 3, 6, 0.5),
(Conv2DDownBlock, {"out_channels": 8}, 3, 8, 0.5),
(MaxPool2DDownBlock, {}, 3, 3, 0.5),
(MaxPool2DDownBlock, {"out_channels": 8}, 3, 3, 0.5),
(ConvTrans2DUpBlock, {}, 4, 2, 2),
(ConvTrans2DUpBlock, {"out_channels": 3}, 4, 3, 2),
(PixelShuffle2DUpBlock, {}, 4, 4, 2),
(PixelShuffle2DUpBlock, {"out_channels": 8}, 4, 4, 2),
(Bilinear2DUpsampleBlock, {}, 3, 3, 2),
(Bilinear2DUpsampleBlock, {"out_channels": 8}, 3, 3, 2),
],
)
def test_output_channels_and_spatial_dimensions(
self, block_type, kwargs, in_channels, expected_out_channels, scale
):
input_tensor = torch.randn(2, in_channels, 12, 18)
block = block_type(in_channels=in_channels, **kwargs)

output = block(input_tensor)

expected_height = int(input_tensor.shape[2] * scale)
expected_width = int(input_tensor.shape[3] * scale)
assert block.out_channels == expected_out_channels
assert output.shape == (
input_tensor.shape[0],
expected_out_channels,
expected_height,
expected_width,
)
assert block.out_h(input_tensor.shape[2]) == output.shape[2]
assert block.out_w(input_tensor.shape[3]) == output.shape[3]
Loading