Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(binding-constraints): create UpdateConstraints command #2274

Open
wants to merge 37 commits into
base: dev
Choose a base branch
from

Conversation

smailio
Copy link
Contributor

@smailio smailio commented Dec 18, 2024

No description provided.

@smailio smailio changed the title Feature/2374 binding constraints command feat(binding-constraints): create UpdateConstraint command Jan 30, 2025
@smailio smailio changed the title feat(binding-constraints): create UpdateConstraint command feat(binding-constraints): create UpdateConstraints command Feb 17, 2025
@smailio smailio marked this pull request as ready for review February 27, 2025 15:34
@smailio smailio requested a review from a team February 27, 2025 16:01
Copy link
Contributor

@MartinBelthle MartinBelthle left a comment

Choose a reason for hiding this comment

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

Once all of this will be handled, we could add small unit tests for this command alone

@@ -141,7 +141,7 @@ def get_binding_constraint_config_cls(study_version: StudyVersion) -> Type[Bindi
return BindingConstraintPropertiesBase


def create_binding_constraint_config(study_version: StudyVersion, **kwargs: Any) -> BindingConstraintProperties:
def create_binding_constraint_props(study_version: StudyVersion, **kwargs: Any) -> BindingConstraintProperties:
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: Rename create_binding_constraint_properties

@@ -169,9 +169,8 @@ def test_update_multiple_binding_constraints(self, client: TestClient, user_acce
res = client.get(f"/v1/studies/{study_id}/commands")
assert res.status_code == 200
json_result = res.json()
assert len(json_result) == 2
Copy link
Contributor

Choose a reason for hiding this comment

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

Change the comments in this test as they are no longer relevant with your change

bcs_output = {}
for bc_id, bc_input in bcs_by_ids.items():
# check binding constraint id sent by user exist for this study
# Note that this check is both done here and when the commmand is applied as well
Copy link
Contributor

Choose a reason for hiding this comment

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

typo: command with 2 m :)

# The user changed the operator, we have to rename matrices accordingly
existing_operator = BindingConstraintOperator(current_value["operator"])
update_matrices_names(file_study, bc_id, existing_operator, value.operator)
# convert payload sent by user (most likely from table mode) to a ConstraintOutput dict
Copy link
Contributor

Choose a reason for hiding this comment

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

you can remove the comment in-between parenthesis

dict: The adapted binding constraint data in the output format.

"""
bc_json_copy = copy.deepcopy(bc_json)
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure to understand why you need a copy here


# Properties of the `UPDATE_BINDING_CONSTRAINT` command:
bc_props_by_id: t.Mapping[str, BindingConstraintProperties]
study_version: StudyVersion
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't need this it's already inside the ICommand class

removed_groups = old_groups - new_groups
remove_bc_from_scenario_builder(file_study, removed_groups)
file_study.tree.save(bcs_json, bcs_url)
return CommandOutput(status=True, message="UpdatedBindingVonstraints command created successfully.")
Copy link
Contributor

Choose a reason for hiding this comment

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

Typo

action=self.command_name.value, args=json_command, version=self.version, study_version=self.study_version
)

def generate_replacement_matrices(
Copy link
Contributor

Choose a reason for hiding this comment

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

You can put this method private and static

study_version: StudyVersion,
bc_props: BindingConstraintProperties,
operator: BindingConstraintOperator,
) -> t.Iterator[t.Tuple[str, t.Union[t.List[t.List[MatrixData]], str]]]:
Copy link
Contributor

Choose a reason for hiding this comment

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

You can type this a t.Iterator[t.Tuple[str, list[list[MatrixData]]]] as the matrix is always a list

bc_id, study_version, bc_props, bc_props.operator
):
# prepare matrix as a dict to save it in the tree
matrix_url = target.split("/")
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems really complicated, why do'nt you just do file_study.tree.save(data=next_matrix, url=matrix_url) ?
Also if it's that simple you could do it inside the generate_replacement_matrices method to lighten the _apply code which is already not that simple

@pull-request-size pull-request-size bot added size/XL and removed size/L labels Mar 4, 2025
Copy link
Contributor

@MartinBelthle MartinBelthle left a comment

Choose a reason for hiding this comment

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

Mostly minor remarks

# More efficient way of doing things but using less readable commands.
commands = []

study_version = StudyVersion.parse(study.version)
Copy link
Contributor

@MartinBelthle MartinBelthle Mar 5, 2025

Choose a reason for hiding this comment

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

Don't need to parse anymore as study is now a StudyInterface, you can just write study.version

return bcs_output

def __convert_constraint_input_to_output(
self, bc_json: JSON, bc_input_as_dict: Dict[str, Any], study_version: StudyVersion
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: JSON = Dict[str, Any] it's a bit weird to use both typings insidethe same method. I would replace JSon by dict[str, Any]

@@ -144,9 +144,9 @@ def test_update_multiple_binding_constraints(self, client: TestClient, user_acce
res = client.get(f"/v1/studies/{study_id}/commands")
assert res.status_code == 200
json_result = res.json()
assert len(json_result) == 10
assert len(json_result) == 1
for cmd in json_result:
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: You can replace the for loop by a check of [0] as you know there's only one element



@pytest.fixture
def file_study():
Copy link
Contributor

Choose a reason for hiding this comment

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

The name of the fixture does not seem to represent what it achieves

@pytest.fixture
def binding_constraint_properties():
return {
"bc_1": ConstraintInput(group="new_group1", operator="greater", time_step="daily"),
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems to me the objects here should be Properties instead of ConstraintInput

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The api is called with ConstraintInput which inherit Properties anyway. It's better to test it with ConstraintInput as it's closer to the reality.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's really minor but I disagree, the command expects Properties and not Input

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ConstraintInput is a Properties

return existing_constraint

study_data.bindings = list(map(update_binding_constraint_dto, study_data.bindings))
return CommandOutput(status=True, message="_apply_config success !"), {}
Copy link
Contributor

Choose a reason for hiding this comment

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

The message is not really important but here it's not clear.
You can see in other commands what we do usually

group = bc_props_as_dict.get("group") or existing_constraint.group
operator = bc_props_as_dict.get("operator") or existing_constraint.operator
time_step = bc_props_as_dict.get("time_step") or existing_constraint.time_step
areas = bc_props_as_dict.get("areas") or existing_constraint.areas
Copy link
Contributor

Choose a reason for hiding this comment

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

Areas and clusters can not be updated as we don't give terms here. You can remove this

)
# It's important to use exclude_unset=True. Otherwise we'd override
# existing values with the default bc_props values.
# Also important to use by_alias=True, so time_step is renamed to type
Copy link
Contributor

Choose a reason for hiding this comment

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

it's not just for time_step, it's for the filtering also. You can remove this line I think

operator: BindingConstraintOperator,
) -> t.Iterator[t.Tuple[str, t.List[t.List[MatrixData]]]]:
"""
Yield one or two (when operator is "BOTH") matrices intialized with default values.
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: IMO a better wording would be Yield one (or two when operator is "BOTH") ...
Also, typo: initialized

BindingConstraintFrequency.DAILY.value: default_bc_weekly_daily_86,
BindingConstraintFrequency.WEEKLY.value: default_bc_weekly_daily_86,
}[bc_props.time_step].tolist()
yield (target, matrix)
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: My IDE says you can just write yield target, matrix

status=False,
message=f"Path '{target}' does not exist.",
)
except AssertionError as e:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

remove except asserion error

status=False,
message=f"Path '{target}' does not target a matrix.",
)
except Exception as e:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

remove this as well

target_matrix = target_matrix[element]
target_matrix[matrix_url[-1]] = next_matrix
file_study.tree.save(replace_matrix_data)
except (KeyError, ChildNotFoundError) as e:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

remove this

study_file_target = "input/bindingconstraints/bindingconstraints"
url = study_file_target.split("/")
tree_node = file_study.tree.get_node(url)
if not isinstance(tree_node, IniFileNode):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

remove this file


excluded_fields = set(ICommand.model_fields)
json_command = self.model_dump(mode="json", exclude=excluded_fields, exclude_unset=True)
# for key in json_command:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

remove bc_props_by_id key

bc_props_by_id: t.Mapping[str, BindingConstraintProperties]

def _apply_config(self, study_data: FileStudyTreeConfig) -> t.Tuple[CommandOutput, t.Dict[str, t.Any]]:
# index = next(i for i, bc in enumerate(study_data.bindings) if bc.id == self.id)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

use same code that update a single constraint. This is usefull to update objects in cache.

# input_bc_props = create_binding_constraint_config(study_version, **bc_input_as_dict)

bc_props_by_id = {
key: create_binding_constraint_config(study_version, **value) for key, value in bc_by_id.items()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

rename this method

@pytest.fixture
def binding_constraint_properties():
return {
"bc_1": ConstraintInput(group="new_group1", operator="greater", time_step="daily"),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The api is called with ConstraintInput which inherit Properties anyway. It's better to test it with ConstraintInput as it's closer to the reality.

Copy link
Contributor

@MartinBelthle MartinBelthle left a comment

Choose a reason for hiding this comment

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

Didn't read everything I assume you took my last comments into account. Only minor remarks / opinions, after that we're good I think

@pytest.fixture
def binding_constraint_properties():
return {
"bc_1": ConstraintInput(group="new_group1", operator="greater", time_step="daily"),
Copy link
Contributor

Choose a reason for hiding this comment

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

It's really minor but I disagree, the command expects Properties and not Input

)
return existing_constraint

existing_ids = {b.id for b in study_data.bindings}
Copy link
Contributor

Choose a reason for hiding this comment

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

I find this really hard to read but we'll rewrite this when we'll remove the _apply_config and it will be easier to write

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just create a set, to check my elements exists in this set it's a common way to do.

bc_id, study.version, value, output.operator, self._command_context
)
commands.extend(replace_matrix_commands)
# convert ConstraintInput to dict, UpdateBindingConstraints will expect a dict as input
Copy link
Contributor

Choose a reason for hiding this comment

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

The command expects Properties not a dict or a ConstraintInput object. IMO either you give the constraintInput as is and you remove the comment or you modify the comment to say you're giving Properties and you really give properties

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants