Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
4 changes: 4 additions & 0 deletions bindings/pyroot/pythonizations/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
# CMakeLists.txt file for building ROOT pythonizations libraries
################################################################

set(PYROOT_EXTRA_PYTHON_SOURCES)

if(dataframe)
list(APPEND PYROOT_EXTRA_PYTHON_SOURCES
ROOT/_pythonization/_rdf_utils.py
Expand Down Expand Up @@ -65,6 +67,8 @@ if(tmva)
endif()
endif()

add_subdirectory(python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras)

list(APPEND PYROOT_EXTRA_HEADERS
inc/TPyDispatcher.h)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
# For the list of contributors see $ROOTSYS/README/CREDITS. #
################################################################################

import sys

Check failure on line 13 in bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (F401)

bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py:13:8: F401 `sys` imported but unused
import cppyy
from cppyy.gbl import gSystem

Check failure on line 15 in bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (F401)

bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py:15:23: F401 `cppyy.gbl.gSystem` imported but unused

from .. import pythonization

Expand All @@ -20,15 +20,15 @@
from ._dataloader import DataLoader
from ._crossvalidation import CrossValidation

from ._rbdt import Compute, pythonize_rbdt

Check failure on line 23 in bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (F401)

bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py:23:29: F401 `._rbdt.pythonize_rbdt` imported but unused; consider removing, adding to `__all__`, or using a redundant alias

Check failure on line 23 in bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (F401)

bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py:23:20: F401 `._rbdt.Compute` imported but unused; consider removing, adding to `__all__`, or using a redundant alias

Check failure on line 23 in bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (I001)

bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py:13:1: I001 Import block is un-sorted or un-formatted


def inject_rbatchgenerator(ns):
from ._batchgenerator import (
CreateNumPyGenerators,
CreateTFDatasets,
CreatePyTorchGenerators,
)

Check failure on line 31 in bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (I001)

bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py:27:5: I001 Import block is un-sorted or un-formatted

python_batchgenerator_functions = [
CreateNumPyGenerators,
Expand All @@ -43,7 +43,8 @@
return ns


from ._gnn import RModel_GNN, RModel_GraphIndependent

Check failure on line 46 in bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (F401)

bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py:46:31: F401 `._gnn.RModel_GraphIndependent` imported but unused; consider removing, adding to `__all__`, or using a redundant alias

Check failure on line 46 in bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (F401)

bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py:46:19: F401 `._gnn.RModel_GNN` imported but unused; consider removing, adding to `__all__`, or using a redundant alias

Check failure on line 46 in bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E402)

bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py:46:1: E402 Module level import not at top of file
from ._sofie._parser._keras.parser import RModelParser_Keras

Check failure on line 47 in bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E402)

bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py:47:1: E402 Module level import not at top of file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But here is the main culprit: this global import is the entry point for all other imports I think.

Copy link
Author

@PrasannaKasar PrasannaKasar Aug 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a question, if we don't expose RModelParser_Keras.Parse class in the _tmva module, then how can we use it using pythonization?
I tried removing it but while using the parser it gave me this error
image
is there a way to tackle this?
@guitargeek @pcanal

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
from ._sofie._parser._keras.parser import RModelParser_Keras


hasRDF = "dataframe" in cppyy.gbl.ROOT.GetROOT().GetConfigFeatures()
if hasRDF:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
if (tmva)
list(APPEND PYROOT_EXTRA_PYTHON_SOURCES
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/__init__.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_functional.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser_test_function.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/__init__.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/batchnorm.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/binary.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/concat.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/conv.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/dense.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/elu.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/layernorm.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leaky_relu.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/permute.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/pooling.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/relu.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/rnn.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/selu.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/sigmoid.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/softmax.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/swish.py
ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/tanh.py)
set(PYROOT_EXTRA_PYTHON_SOURCES "${PYROOT_EXTRA_PYTHON_SOURCES}" PARENT_SCOPE)
endif()
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
def get_keras_version() -> str:

import keras

return keras.__version__

keras_version = get_keras_version()
Copy link
Contributor

@guitargeek guitargeek Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks much better already!

But there is still an implicit import of Keras in this place. You do this import in _tmva/__init__.py:

from ._sofie._parser._keras.parser import RModelParser_Keras

Importing a submodule implies importing all parent modules too. You you're also importing ._sofie._parser._keras. This import means running _keras/__init__.py, where you execute get_keras_version(), thus implicitly importing Keras.

You can fix this by removing the line with keras_version = get_keras_version(), and replace keras_version with get_keras_version() in the remaining code.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh i overlooked this @guitargeek
is this the correct way to do it then?

image image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @guitargeek i have made the changes
Can you please verify it once and start the tests?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@guitargeek I found the error
image
here, the CI uses python 3.9 and a Keras version older than 2.13
And that's why it might be saying that tensorflow.python.framework.errors_impl.InvalidArgumentError: Graph execution error in case of AveragePool2D

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this sofie-keras-parser test to validate the parser's functionality

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies I didn't catch this before
Can I know which keras version does the CI use?
I'll run the tests locally then

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The oldest Keras version in use by the CI is keras-2.13.1, on the alma8 platform. That's because this platform still uses Python 3.8.

But let's try to first try to fix the problems on the other platforms that use keras>=3.0, and then you think about if you need to support the older Keras versions later with @sanjibansg. Personally, I think it's totally valid to disable the functionality for Keras 2, as Keras 3 is out for almost 2 years.

If you need to know, you can check the version of Python packages that each platform is using by looking at the logs of the Docker container creation:
https://github.com/root-project/root-ci-images/actions

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks
I'll look into it

Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import numpy as np

def generate_keras_functional(dst_dir):

from keras import models, layers

# Helper training function
def train_and_save(model, name):
# Handle multiple inputs dynamically
if isinstance(model.input_shape, list):
x_train = [np.random.rand(32, *shape[1:]) for shape in model.input_shape]
else:
x_train = np.random.rand(32, *model.input_shape[1:])
y_train = np.random.rand(32, *model.output_shape[1:])

model.compile(optimizer='adam', loss='mean_squared_error', metrics=['mae'])
model.fit(x_train, y_train, epochs=1, verbose=0)
model.save(f"{dst_dir}/Functional_{name}_test.h5")

# Activation Functions
for act in ['relu', 'elu', 'leaky_relu', 'selu', 'sigmoid', 'softmax', 'swish', 'tanh']:
inp = layers.Input(shape=(10,))
out = layers.Activation(act)(inp)
model = models.Model(inp, out)
train_and_save(model, f"Activation_layer_{act.capitalize()}")
# Along with these, Keras allows explicit delcaration of activation layers such as:
# [ELU, ReLU, LeakyReLU, Softmax]

# Add
in1 = layers.Input(shape=(8,))
in2 = layers.Input(shape=(8,))
out = layers.Add()([in1, in2])
model = models.Model([in1, in2], out)
train_and_save(model, "Add")

# AveragePooling2D channels_first
inp = layers.Input(shape=(3, 8, 8))
out = layers.AveragePooling2D(pool_size=(2, 2), data_format='channels_first')(inp)
model = models.Model(inp, out)
train_and_save(model, "AveragePooling2D_channels_first")

# AveragePooling2D channels_last
inp = layers.Input(shape=(8, 8, 3))
out = layers.AveragePooling2D(pool_size=(2, 2), data_format='channels_last')(inp)
model = models.Model(inp, out)
train_and_save(model, "AveragePooling2D_channels_last")

# BatchNorm
inp = layers.Input(shape=(10, 3, 5))
out = layers.BatchNormalization(axis=2)(inp)
model = models.Model(inp, out)
train_and_save(model, "BatchNorm")

# Concat
in1 = layers.Input(shape=(8,))
in2 = layers.Input(shape=(8,))
out = layers.Concatenate()([in1, in2])
model = models.Model([in1, in2], out)
train_and_save(model, "Concat")

# Conv2D channels_first
inp = layers.Input(shape=(3, 8, 8))
out = layers.Conv2D(4, (3, 3), padding='same', data_format='channels_first', activation='relu')(inp)
model = models.Model(inp, out)
train_and_save(model, "Conv2D_channels_first")

# Conv2D channels_last
inp = layers.Input(shape=(8, 8, 3))
out = layers.Conv2D(4, (3, 3), padding='same', data_format='channels_last', activation='leaky_relu')(inp)
model = models.Model(inp, out)
train_and_save(model, "Conv2D_channels_last")

# Conv2D padding_same
inp = layers.Input(shape=(8, 8, 3))
out = layers.Conv2D(4, (3, 3), padding='same', data_format='channels_last')(inp)
model = models.Model(inp, out)
train_and_save(model, "Conv2D_padding_same")

# Conv2D padding_valid
inp = layers.Input(shape=(8, 8, 3))
out = layers.Conv2D(4, (3, 3), padding='valid', data_format='channels_last', activation='elu')(inp)
model = models.Model(inp, out)
train_and_save(model, "Conv2D_padding_valid")

# Dense
inp = layers.Input(shape=(10,))
out = layers.Dense(5, activation='tanh')(inp)
model = models.Model(inp, out)
train_and_save(model, "Dense")

# ELU
inp = layers.Input(shape=(10,))
out = layers.ELU(alpha=0.5)(inp)
model = models.Model(inp, out)
train_and_save(model, "ELU")

# Flatten
inp = layers.Input(shape=(4, 5))
out = layers.Flatten()(inp)
model = models.Model(inp, out)
train_and_save(model, "Flatten")

# GlobalAveragePooling2D channels first
inp = layers.Input(shape=(3, 4, 6))
out = layers.GlobalAveragePooling2D(data_format='channels_first')(inp)
model = models.Model(inp, out)
train_and_save(model, "GlobalAveragePooling2D_channels_first")

# GlobalAveragePooling2D channels last
inp = layers.Input(shape=(4, 6, 3))
out = layers.GlobalAveragePooling2D(data_format='channels_last')(inp)
model = models.Model(inp, out)
train_and_save(model, "GlobalAveragePooling2D_channels_last")

# LayerNorm
inp = layers.Input(shape=(10, 3, 5))
out = layers.LayerNormalization(axis=-1)(inp)
model = models.Model(inp, out)
train_and_save(model, "LayerNorm")

# LeakyReLU
inp = layers.Input(shape=(10,))
out = layers.LeakyReLU()(inp)
model = models.Model(inp, out)
train_and_save(model, "LeakyReLU")

# MaxPooling2D channels_first
inp = layers.Input(shape=(3, 8, 8))
out = layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_first')(inp)
model = models.Model(inp, out)
train_and_save(model, "MaxPool2D_channels_first")

# MaxPooling2D channels_last
inp = layers.Input(shape=(8, 8, 3))
out = layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_last')(inp)
model = models.Model(inp, out)
train_and_save(model, "MaxPool2D_channels_last")

# Multiply
in1 = layers.Input(shape=(8,))
in2 = layers.Input(shape=(8,))
out = layers.Multiply()([in1, in2])
model = models.Model([in1, in2], out)
train_and_save(model, "Multiply")

# Permute
inp = layers.Input(shape=(3, 4, 5))
out = layers.Permute((2, 1, 3))(inp)
model = models.Model(inp, out)
train_and_save(model, "Permute")

# ReLU
inp = layers.Input(shape=(10,))
out = layers.ReLU()(inp)
model = models.Model(inp, out)
train_and_save(model, "ReLU")

# Reshape
inp = layers.Input(shape=(4, 5))
out = layers.Reshape((2, 10))(inp)
model = models.Model(inp, out)
train_and_save(model, "Reshape")

# Softmax
inp = layers.Input(shape=(10,))
out = layers.Softmax()(inp)
model = models.Model(inp, out)
train_and_save(model, "Softmax")

# Subtract
in1 = layers.Input(shape=(8,))
in2 = layers.Input(shape=(8,))
out = layers.Subtract()([in1, in2])
model = models.Model([in1, in2], out)
train_and_save(model, "Subtract")

# Layer Combination

inp = layers.Input(shape=(32, 32, 3))
x = layers.Conv2D(8, (3,3), padding="same", activation="relu")(inp)
x = layers.MaxPooling2D((2,2))(x)
x = layers.Reshape((16, 16, 8))(x)
x = layers.Permute((3, 1, 2))(x)
x = layers.Flatten()(x)
out = layers.Dense(10, activation="softmax")(x)
model = models.Model(inp, out)
train_and_save(model, "Layer_Combination_1")

inp = layers.Input(shape=(20,))
x = layers.Dense(32, activation="tanh")(inp)
x = layers.Dense(16)(x)
x = layers.ELU()(x)
x = layers.LayerNormalization()(x)
out = layers.Dense(5, activation="sigmoid")(x)
model = models.Model(inp, out)
train_and_save(model, "Layer_Combination_2")


inp1 = layers.Input(shape=(16,))
inp2 = layers.Input(shape=(16,))
d1 = layers.Dense(16, activation="relu")(inp1)
d2 = layers.Dense(16, activation="selu")(inp2)
add = layers.Add()([d1, d2])
sub = layers.Subtract()([d1, d2])
mul = layers.Multiply()([d1, d2])
merged = layers.Concatenate()([add, sub, mul])
merged = layers.LeakyReLU(alpha=0.1)(merged)
out = layers.Dense(4, activation="softmax")(merged)
model = models.Model([inp1, inp2], out)
train_and_save(model, "Layer_Combination_3")
Loading
Loading