Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
49 changes: 37 additions & 12 deletions DashAI/back/dependencies/registry/component_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,33 @@ def _get_base_type(self, new_component: type) -> str:

return base_classes_cantidates[0].TYPE

@staticmethod
@beartype
def _collect_compatible_components(component: type) -> List[str]:
"""Collect the union of ``COMPATIBLE_COMPONENTS`` declared along the MRO.

Each class in the component's MRO contributes only the entries it
declares itself, so mixins and base classes compose instead of the
first declaration shadowing the rest (e.g. a task mixin declaring the
task plus a model base class declaring its supported explainers).

Parameters
----------
component : type
The component class to inspect.

Returns
-------
List[str]
Deduplicated compatible component names in MRO order.
"""
compatible_components: List[str] = []
for klass in component.__mro__:
for entry in vars(klass).get("COMPATIBLE_COMPONENTS", []):
if entry not in compatible_components:
compatible_components.append(entry)
return compatible_components

@beartype
def register_component(self, new_component: Type) -> None:
"""Register a component within the registry.
Expand Down Expand Up @@ -225,12 +252,11 @@ def register_component(self, new_component: Type) -> None:
else:
self._registry[base_type][new_component.__name__] = new_register_component

if hasattr(new_component, "COMPATIBLE_COMPONENTS"):
for compatible_component in new_component.COMPATIBLE_COMPONENTS:
self._relationship_manager.add_relationship(
new_component.__name__,
compatible_component,
)
for compatible_component in self._collect_compatible_components(new_component):
self._relationship_manager.add_relationship(
new_component.__name__,
compatible_component,
)

@beartype
def unregister_component(self, component: Type) -> None:
Expand All @@ -254,12 +280,11 @@ def unregister_component(self, component: Type) -> None:
f"in the registry. Exception: {e}"
) from e

if hasattr(component, "COMPATIBLE_COMPONENTS"):
for compatible_component in component.COMPATIBLE_COMPONENTS:
self._relationship_manager.remove_relationship(
component.__name__,
compatible_component,
)
for compatible_component in self._collect_compatible_components(component):
self._relationship_manager.remove_relationship(
component.__name__,
compatible_component,
)

@beartype
def get_components_by_types(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default function InlineExplainerCreator({
const { t } = useTranslation(["explainers", "common"]);
const formSubmitRef = useRef(null);

const { runId, taskName } = explainerConfig;
const { runId, taskName, modelName } = explainerConfig;
const isLocal = scope === "local";

const defaultNewExplainer = useMemo(
Expand Down Expand Up @@ -309,6 +309,7 @@ export default function InlineExplainerCreator({
setNextEnabled={setNextEnabled}
scope={isLocal ? "Local" : "Global"}
taskName={taskName}
modelName={modelName}
existingExplainers={existingExplainers}
/>
)}
Expand Down Expand Up @@ -368,6 +369,7 @@ InlineExplainerCreator.propTypes = {
explainerConfig: PropTypes.shape({
runId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
taskName: PropTypes.string,
modelName: PropTypes.string,
}).isRequired,
onCreated: PropTypes.func,
onCancel: PropTypes.func.isRequired,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export default function NewGlobalExplainerModal({

const { enqueueSnackbar } = useSnackbar();

const { runId, taskName } = explainerConfig;
const { runId, taskName, modelName } = explainerConfig;

const defaultNewGlobalExpl = {
name: "",
Expand Down Expand Up @@ -331,6 +331,7 @@ export default function NewGlobalExplainerModal({
setNextEnabled={setNextEnabled}
scope={"Global"}
taskName={taskName}
modelName={modelName}
existingExplainers={existingGlobalExplainers}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export default function NewLocalExplainerModal({

const { enqueueSnackbar } = useSnackbar();

const { runId, taskName } = explainerConfig;
const { runId, taskName, modelName } = explainerConfig;

const defaultNewLocalExpl = {
name: "",
Expand Down Expand Up @@ -337,6 +337,7 @@ export default function NewLocalExplainerModal({
setNextEnabled={setNextEnabled}
scope={"Local"}
taskName={taskName}
modelName={modelName}
existingExplainers={existingLocalExplainers}
/>
)}
Expand Down
25 changes: 22 additions & 3 deletions DashAI/front/src/components/explainers/SetNameAndExplainerStep.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function SetNameAndExplainerStep({
setNextEnabled,
scope,
taskName,
modelName,
existingExplainers = [],
}) {
const { enqueueSnackbar } = useSnackbar();
Expand All @@ -31,9 +32,26 @@ function SetNameAndExplainerStep({
const getExplainers = async () => {
setLoading(true);
try {
const result = await getComponentsRequest({
selectTypes: [`${scope}Explainer`],
relatedComponent: taskName,
// Explainers related to the task are model agnostic (usable by any
// model of the task); explainers related to the run's model are
// model specific ones the model declares in COMPATIBLE_COMPONENTS.
const [taskRelated, modelRelated] = await Promise.all([
getComponentsRequest({
selectTypes: [`${scope}Explainer`],
relatedComponent: taskName,
}),
modelName
? getComponentsRequest({
selectTypes: [`${scope}Explainer`],
relatedComponent: modelName,
})
: Promise.resolve([]),
]);
const seen = new Set();
const result = [...taskRelated, ...modelRelated].filter((obj) => {
if (seen.has(obj.name)) return false;
seen.add(obj.name);
return true;
});
setExplainers(result.filter((obj) => !obj.name.startsWith("Fit")));
} catch (error) {
Expand Down Expand Up @@ -165,6 +183,7 @@ SetNameAndExplainerStep.propTypes = {
setNextEnabled: PropTypes.func.isRequired,
scope: PropTypes.string.isRequired,
taskName: PropTypes.string,
modelName: PropTypes.string,
existingExplainers: PropTypes.array,
};

Expand Down
2 changes: 2 additions & 0 deletions DashAI/front/src/components/models/RunResults.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ export default function RunResults({
explainerConfig={{
runId: run.id,
taskName: session?.task_name,
modelName: run.model_name,
}}
onCreated={handleExplainerCreated}
onCancel={() => setGlobalCreatorOpen(false)}
Expand All @@ -436,6 +437,7 @@ export default function RunResults({
explainerConfig={{
runId: run.id,
taskName: session?.task_name,
modelName: run.model_name,
}}
onCreated={handleExplainerCreated}
onCancel={() => setLocalCreatorOpen(false)}
Expand Down
45 changes: 45 additions & 0 deletions tests/back/registries/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ class ComponentWithTwoBaseClasses(BaseConfigComponent1, BaseConfigComponent2): .
class NoComponent: ...


class RelatedMixin:
COMPATIBLE_COMPONENTS = ["Component1"]


class RelatedParentComponent(BaseStaticComponent):
COMPATIBLE_COMPONENTS = ["Component2"]


class CombinedRelatedComponent(RelatedMixin, RelatedParentComponent):
# Redeclares an inherited entry on purpose: the union must deduplicate.
COMPATIBLE_COMPONENTS = ["Component1"]


COMPONENT1_DICT = {
"name": "Component1",
"type": "ConfigComponent1",
Expand Down Expand Up @@ -482,3 +495,35 @@ def test_relationships_module():
COMPONENT1_DICT,
COMPONENT2_DICT,
]


def test_compatible_components_merge_across_bases():
test_registry = ComponentRegistry(
initial_components=[
Component1,
Component2,
]
)

test_registry.register_component(CombinedRelatedComponent)

# The mixin contributes "Component1", the static base "Component2";
# the subclass redeclaration of "Component1" does not duplicate it.
assert test_registry._relationship_manager["CombinedRelatedComponent"] == [
"Component1",
"Component2",
]
assert test_registry.get_related_components("CombinedRelatedComponent") == [
COMPONENT1_DICT,
COMPONENT2_DICT,
]
assert [
component["name"]
for component in test_registry.get_related_components("Component2")
] == ["CombinedRelatedComponent"]

# Unregistering removes both inherited edges symmetrically.
test_registry.unregister_component(CombinedRelatedComponent)
assert test_registry._relationship_manager["CombinedRelatedComponent"] == []
assert test_registry.get_related_components("Component1") == []
assert test_registry.get_related_components("Component2") == []
Loading