diff --git a/.gitignore b/.gitignore index e2d11a0..65f6763 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,16 @@ ENV/ __gams* index*.bin -*.ilp \ No newline at end of file +*.ilp + +# Ignore local testing +create_code_testset.py +create_tool_testset.py +grading.py +run_exp.py +show_results.ipynb +quick_test.ipynb +tmp/ + +# Ignore kb +chroma_langchain_db/ \ No newline at end of file diff --git a/agents.py b/agents.py deleted file mode 100644 index 259e224..0000000 --- a/agents.py +++ /dev/null @@ -1,977 +0,0 @@ -import copy -import time -from typing import Dict, Optional, Union, List -from openai import Client, OpenAI -from prompts import get_prompts -from internal_tools import feasibility_restoration, sensitivity_analysis, components_retrival, evaluate_modification -from internal_tools import syntax_guidance, fnArgsDecoder -from extractor import extract_component_descriptions, insert_code, run_with_exec -import json -import re -#import streamlit as st - - -class Agent: - def __init__(self, name, description, client, llm="gpt-4-turbo-preview", **kwargs): - self.name = name - self.description = description - self.client = client - self.system_prompt = "You're a helpful assistant." - self.kwargs = kwargs - self.llm = llm - - self.function_names = kwargs.get('function_names', None) - self.tools = kwargs.get('tools', None) - self.multiple_tools = kwargs.get('multiple_tools', None) - self.single_tools = kwargs.get('single_tools', None) - self.none_tools = kwargs.get('none_tools', None) - self.all_tools = kwargs.get('all_tools', None) - self.tool_choice = kwargs.get('tool_choice', None) - self.syntax_guidance_tool = kwargs.get('syntax_guidance_tool', None) - - self.team_conversation_filename = './logs/team_conversation.txt' - self.chat_history_filename = './logs/detailed_chat_history.txt' - - def llm_call(self, prompt: Optional[str] = None, messages: Optional[List] = None, - seed: int = 10, stream: bool = False) -> str: - # make sure exactly one of prompt or messages is provided - assert (prompt is None) != (messages is None) - # make sure if messages is provided, it is a list of dicts with role and content - if messages is not None: - assert isinstance(messages, list) - for message in messages: - assert isinstance(message, dict) - assert "role" in message - assert "content" in message - - if not prompt is None: - messages = [ - {"role": "system", "content": self.system_prompt}, - {"role": "user", "content": prompt}, - ] - - # print("=" * 10) - # print(f'llm_call is called, the following messages are sent to the llm: ') - # for message in messages: - # print(f'{message["role"]}: {message["content"]}') - # print("=" * 10) - - if type(self.client) in [OpenAI, Client]: - completion = self.client.chat.completions.create( - model=self.llm, - messages=messages, - seed=seed, - stream=stream, - ) - - if stream: - return completion - else: - content = completion.choices[0].message.content - return content - - @staticmethod - def generate_pseudo_messages(messages: List[Dict], team_conversation: List[Dict], - new_prompt: str) -> List[Dict]: - pseudo_messages = copy.deepcopy(messages) - if team_conversation: - for message in team_conversation: - if message["agent_name"] in ['Syntax reminder', 'Code reminder']: - pseudo_messages.append({"role": "system", - "content": f'{message["agent_name"]}: \n\n' + - message["agent_response"]}) - else: - pseudo_messages.append({"role": "assistant", - "content": f'I am {message["agent_name"]} in Assistant Team. \n\n' + - message["agent_response"]}) - pseudo_messages.append({"role": "user", "content": new_prompt}) - return pseudo_messages - - def save_team_conversation(self, team_conversation): - with open(self.team_conversation_filename, 'a') as f: - for message in team_conversation: - f.write(f"{message['agent_name']}: {message['agent_response']}\n\n") - - def print_in_and_out(self, prompt, llm_response, agent_name=None): - if agent_name is None: - agent_name = self.name - print("=" * 5 + agent_name + "=" * 5) - print('-' * 5 + 'prompt:' + '-' * 5) - print(prompt) - print('-' * 5 + 'llm_response:' + '-' * 5) - print(llm_response) - - def llm_call_exp(self, prompt: Optional[str] = None, messages: Optional[List] = None, - seed: int = 10, temperature: float = 0.1, - json_mode: bool = False, - stream: bool = False,) -> str: - # make sure exactly one of prompt or messages is provided - assert (prompt is None) != (messages is None) - # make sure if messages is provided, it is a list of dicts with role and content - if messages is not None: - assert isinstance(messages, list) - for message in messages: - assert isinstance(message, dict) - assert "role" in message - assert "content" in message - - if not prompt is None: - messages = [ - {"role": "system", "content": self.system_prompt}, - {"role": "user", "content": prompt}, - ] - - if json_mode: - response_format = {"type": "json_object"} - else: - response_format = {"type": "text"} - - if type(self.client) in [OpenAI, Client]: - if self.llm not in ["o3"]: - completion = self.client.chat.completions.create( - model=self.llm, - messages=messages, - seed=seed, - temperature=temperature, - response_format=response_format, - stream=stream, - ) - else: - completion = self.client.chat.completions.create( - model=self.llm, - messages=messages, - seed=seed, - response_format=response_format, - stream=stream, - ) - - if stream: - return completion - else: - content = completion.choices[0].message.content - return content - - -class Interpreter(Agent): - def __init__(self, client: Client, **kwargs): - super().__init__( - name="Interpreter", - description="This is an operations research agent that is an expert in interpreting optimization models and codes to non-experts.", - client=client, - **kwargs, - ) - - self._init_prompt_template() - - def _init_prompt_template(self): - self.interpretation_prompt_template = get_prompts("model_interpretation_prompt") - self.need2describe_prompt_template = get_prompts("need2describe_prompt") - self.interpretation_json_template = get_prompts("model_interpretation_json") - - self.illustration_prompt_template = get_prompts("model_illustration_prompt") - self.inference_prompt_template = get_prompts("model_inference_prompt") - - def _cat(self, cat_need2describe, component_names, component_type): - return cat_need2describe + self.need2describe_prompt_template.format(component_type=component_type, - component_names=component_names) - - def _cut(self, component_type): - if component_type in self.interpretation_json_template["components"]: - del self.interpretation_json_template["components"][component_type] - - def generate_interpretation(self, models_dict: Dict, code: str, model_name="model_1"): - task_complete = False - cnt = 3 - while not task_complete and cnt > 0: - self._init_prompt_template() - cat_need2describe_prompt = "" - need2describe = {} - for component_type in ['sets', 'parameters', 'variables', 'constraints', 'objective']: - need2describe[component_type] = [] - for key, value in models_dict[model_name]["components"][component_type].items(): - if value.get('description') in ['None', None]: - need2describe[component_type].append(key) - # if there are components that haven't been described, add them to the prompt - if len(need2describe[component_type]) > 0: - cat_need2describe_prompt = self._cat(cat_need2describe_prompt, - need2describe[component_type], component_type) - else: - self._cut(component_type) - # print('===' * 10) - # print('cat_need2describe_prompt:', cat_need2describe_prompt) - # print(f'interpretation_json_template: {self.interpretation_json_template}') - - if len(cat_need2describe_prompt) > 0: - model_interpretation_json = json.dumps(self.interpretation_json_template, indent=4) - # create complete prompt with components that haven't been described only - prompt = self.interpretation_prompt_template.format(code=code, - cat_need2describe_prompt=cat_need2describe_prompt, - model_interpretation_json=model_interpretation_json) - else: - # if all the components in all the component types have been described, then no need to call interpreter - return models_dict - - cnt -= 1 - try: - interpretation_json = self.llm_call(prompt=prompt, seed=cnt, stream=False) - print("=" * 10) - print(f'generate_interpretation... cnt left = {cnt}/3') - print(interpretation_json) - print("=" * 10) - output = interpretation_json - # delete until the first '```json' - if "```json" in output: - output = output[output.find("```json") + 7:] - output = output[: output.rfind("```")] - - start = output.find("{") - end = output.rfind("}") - output = output[start:end + 1] - - update = json.loads(output) - - task_complete = True # mark as complete first, if any component incorrect, mark as incomplete - for key in update["components"]: - print(f'Interpreting {key}') - for component in update["components"][key]: - print(f'component: {component}') - # update models_dict with the new descriptions if format is correct, - # next time less components will be included in the prompt - if ('name' in component) and ('description' in component): - models_dict[model_name]["components"][key][component["name"]]["description"] = component[ - "description"] - else: - print(f'Invalid component format marked!, {component}') - task_complete = False - - except Exception as e: - import traceback - - print(traceback.format_exc()) - print("=" * 10) - print(f'generate_interpretation error... cnt left = {cnt}/3') - print(e) - print("=" * 10) - print(f'generate_interpretation prompt that caused the error: ') - print(prompt) - print("=" * 10) - print(interpretation_json) - print("=" * 10) - print( - f"Invalid json format!\n{e}\n Try again ..." - ) - if cnt == 0: - raise Exception("Invalid json format, Failed 3 times!") - return models_dict - - def generate_illustration(self, model_representation: Dict): - prompt = self.illustration_prompt_template.format( - json_representation=model_representation) - print("=" * 10) - print(f'generate_illustration... ') - print("=" * 10) - stream = self.llm_call(prompt=prompt, stream=True) - return stream - - def generate_inference(self, model_representation: Dict): - def split_representation(representation): - # just split session_state.models_dict["model_representation"] into two parts - reduced_json_representation = copy.deepcopy(representation) - del reduced_json_representation["iis"] - del reduced_json_representation["iis_description"] - return representation["iis_description"], reduced_json_representation - - iis_info, reduced_model_representation = split_representation(model_representation) - prompt = self.inference_prompt_template.format( - iis_info=iis_info, - json_representation=reduced_model_representation) - print("=" * 10) - print(f'generate_inference... ') - print("=" * 10) - stream = self.llm_call(prompt=prompt, stream=True) - return stream - - def generate_interpretation_exp(self, args, models_dict: Dict, code: str, model_name="model_1"): - task_complete = False - cnt = 3 - while not task_complete and cnt > 0: - self._init_prompt_template() - cat_need2describe_prompt = "" - need2describe = {} - for component_type in ['sets', 'parameters', 'variables', 'constraints', 'objective']: - need2describe[component_type] = [] - for key, value in models_dict[model_name]["components"][component_type].items(): - if value.get('description') in ['None', None]: - need2describe[component_type].append(key) - # if there are components that haven't been described, add them to the prompt - if len(need2describe[component_type]) > 0: - cat_need2describe_prompt = self._cat(cat_need2describe_prompt, - need2describe[component_type], component_type) - else: - self._cut(component_type) - - if len(cat_need2describe_prompt) > 0: - model_interpretation_json = json.dumps(self.interpretation_json_template, indent=4) - # create complete prompt with components that haven't been described only - prompt = self.interpretation_prompt_template.format(code=code, - cat_need2describe_prompt=cat_need2describe_prompt, - model_interpretation_json=model_interpretation_json) - else: - # if all the components in all the component types have been described, then no need to call interpreter - task_complete = True - return models_dict, cnt, task_complete - - cnt -= 1 - try: - interpretation_json = self.llm_call_exp(prompt=prompt, seed=cnt, - temperature=args.temperature, - json_mode=args.json_mode, stream=False) - print("=" * 10) - print(f'generate_interpretation... cnt left = {cnt}/3') - print(interpretation_json) - print("=" * 10) - output = interpretation_json - - # print("=" * 10 + 'debug: for testing json mode only' + "=" * 10) - # print(output) - # print("=" * 10) - - # delete until the first '```json' - if "```json" in output: - output = output[output.find("```json") + 7:] - output = output[: output.rfind("```")] - - start = output.find("{") - end = output.rfind("}") - output = output[start:end + 1] - - update = json.loads(output) - - task_complete = True # mark as complete first, if any component incorrect, mark as incomplete - for key in update["components"]: - print(f'Interpreting {key}') - for component in update["components"][key]: - print(f'component: {component}') - # update models_dict with the new descriptions if format is correct, - # next time less components will be included in the prompt - if ('name' in component) and ('description' in component): - models_dict[model_name]["components"][key][component["name"]]["description"] = component[ - "description"] - else: - print(f'Invalid component format marked!, {component}') - task_complete = False - - except Exception as e: - import traceback - - print(traceback.format_exc()) - print("=" * 10) - print(f'generate_interpretation error... cnt left = {cnt}/3') - print(e) - print("=" * 10) - print( - f"Invalid json format!\n{e}\n Try again ..." - ) - if cnt == 0: - return models_dict, cnt, task_complete - return models_dict, cnt, task_complete - - def generate_illustration_exp(self, args, model_representation: Dict): - prompt = self.illustration_prompt_template.format( - json_representation=model_representation) - print("=" * 10) - print(f'generate_illustration... ') - print("=" * 10) - stream_or_completion = self.llm_call_exp(prompt=prompt, temperature=args.temperature, - stream=args.illustration_stream) - return stream_or_completion - - def generate_inference_exp(self, args, model_representation: Dict): - def split_representation(representation): - # just split session_state.models_dict["model_representation"] into two parts - reduced_json_representation = copy.deepcopy(representation) - del reduced_json_representation["iis"] - del reduced_json_representation["iis_description"] - return representation["iis_description"], reduced_json_representation - - iis_info, reduced_model_representation = split_representation(model_representation) - prompt = self.inference_prompt_template.format( - iis_info=iis_info, - json_representation=reduced_model_representation) - print("=" * 10) - print(f'generate_inference... ') - print("=" * 10) - stream_or_completion = self.llm_call_exp(prompt=prompt, temperature=args.temperature, - stream=args.inference_stream) - return stream_or_completion - - -class Coordinator(Agent): - def __init__( - self, client: Client, agents: [Agent], max_rounds: int = 5, **kwargs - ): - super().__init__( - name="Coordinator", - description="This is a coordinator agent that chooses which agent to work on the problem next and organizes " - "the conversation within its team. ", - client=client, - **kwargs, - ) - - self.agents = agents - self.max_rounds = max_rounds - - self.coordination_time = 0 - self.coordinator_success = False - self._init_cnt() - self._init_prompt_template() - - def _init_cnt(self): - self.coordinator_cnt = 3 - self.coordinator_success = False - - def _init_prompt_template(self): - self.prompt_template = get_prompts("coordinator_prompt") - self.agents_list = "".join( - [ - "-" + agent.name + ": " + agent.description + "\n" - for agent in self.agents - ] - ) - - def generate_decision(self, messages, team_conversation, agent_name, task): - status = 'In Progress' - - coordinate_prompt = self.prompt_template.format(agents=self.agents_list) - pseudo_messages = self.generate_pseudo_messages(messages, team_conversation, coordinate_prompt) - - cnt = 3 - while cnt > 0: - try: - response = self.llm_call(messages=pseudo_messages, seed=cnt) - decision = response.strip() - if "```json" in decision: - decision = decision.split("```json")[1].split("```")[0] - decision = decision.replace("\\", "") - - self.print_in_and_out(coordinate_prompt, response) - print('Decision:', decision) - - decision = json.loads(decision) - - if team_conversation: - # safeguard to prevent the coordinator from calling the agent - # after the user's query has been answered by explainer - if team_conversation[-1]["agent_name"] == "Explainer": - status = 'Completed' - OptiChat_out = team_conversation[-1]['agent_response'] - if "DONE" in decision.values(): - print("DONE, the user's query is answered.") - else: - print("DONE, the user's query is answered, though the coordinator did not output 'DONE'.") - return status, OptiChat_out - if "DONE" in decision.values() and team_conversation[-1]["agent_name"] == "Engineer": - decision = {'agent_name': 'Explainer', 'task': 'explain the technical feedback'} - - else: - # the first round of the conversation - if "DONE" in decision.values(): - # sometimes user does not ask a question (e.g. saying 'thank you') - # and coordinator considers no query there and outputs 'DONE' directly - decision = {'agent_name': 'Explainer', 'task': 'respond to the user'} - - agent_name.text(decision["agent_name"]) - task.text(decision["task"]) - - return status, decision - - except Exception as e: - print(e) - cnt -= 1 - print("Invalid decision. Trying again ...") - - task.text(f'distribution failed ({cnt}/3)') - - if cnt == 0: - import traceback - err = traceback.format_exc() - print(err) - - status = 'Terminated' - OptiChat_out = "LLM failed to assign tasks to experts! \n" + "Error: " + err + "\n" - - return status, OptiChat_out - - def generate_decision_exp(self, args, messages, team_conversation): - self._init_cnt() - coordinate_prompt = self.prompt_template.format(agents=self.agents_list) - while self.coordinator_cnt > 0: - # messages will only be updated outside the loop (in the OptiChat workflow fn) - # team_conversation will be updated inside the loop (in the Engineer and Explainer fns) - pseudo_messages = self.generate_pseudo_messages(messages, team_conversation, coordinate_prompt) - try: - # in current design, if coordinator has assigned the task once, - # actually there will be no need to call llm to generate the decision again - if team_conversation: - decision = {'agent_name': 'Explainer', 'task': 'explain the technical feedback'} - # last_agent = team_conversation[-1]["agent_name"] - # if last_agent == "Engineer": - # decision = {'agent_name': 'Explainer', 'task': 'explain the technical feedback'} - else: - response = self.llm_call_exp(messages=pseudo_messages, seed=self.coordinator_cnt, - temperature=args.temperature, - json_mode=args.json_mode, stream=False) - decision = response.strip() - if "```json" in decision: - decision = decision.split("```json")[1].split("```")[0] - decision = decision.replace("\\", "") - - print('Decision:', decision) - - decision = json.loads(decision) - assert "agent_name" in decision - assert decision["agent_name"] in [agent.name for agent in self.agents] - assert "task" in decision - - # in the first round of the conversation - # sometimes user does not ask a question (e.g. saying 'thank you') - # and coordinator considers no query there and outputs 'DONE' directly - if "DONE" in decision.values(): - decision = {'agent_name': 'Explainer', 'task': 'respond to the user'} - - self.coordinator_success = True - return decision - - except Exception as e: - print(e) - self.coordinator_cnt -= 1 - print("Invalid decision. Trying again ...") - - if self.coordinator_cnt == 0: - import traceback - err = traceback.format_exc() - print(err) - return None - - -class Explainer(Agent): - def __init__( - self, client: Client, max_rounds: int = 5, **kwargs - ): - super().__init__( - name="Explainer", - description="This is an explainer agent whose task is to either (1) directly answer user queries if the questions can be analyzed through natural language only, or (2) summarize the technical feedback obtained from engineers to answer user queries", - client=client, - **kwargs, - ) - self.explanation_time = 0 - self._init_prompt_template() - - def _init_prompt_template(self): - self.prompt_template = get_prompts("explainer_prompt") - - def generate_explanation_exp(self, args, messages, team_conversation): - prompt = self.prompt_template # nothing to format here - pseudo_messages = self.generate_pseudo_messages(messages, team_conversation, prompt) - - stream_or_completion = self.llm_call_exp(messages=pseudo_messages, temperature=args.temperature, - stream=args.explanation_stream) - return stream_or_completion - - -class Engineer(Agent): - def __init__(self, client: Client, **kwargs): - super().__init__( - name="Engineer", - description="This is an engineer agent whose task is to execute tools and functions when user's query requires an interaction with optimization model. The engineer agent provides technical feedback instead of natural-language explanations." - "Note that some ‘why’ questions are better answered with technical feedback." - "These questions often involve scenarios that differ from the current model.", - client=client, - **kwargs, - ) - - self.pattern = r"```[ \t]*(\w+)?[ \t]*\r?\n(.*?)\r?\n[ \t]*```" - - self._init_prompt_template() - - self.syntax_time = 0 - self.programing_time = 0 - self.evaluation_time = 0 - - self.programmer_cnt = 3 - self.evaluator_cnt = 3 - - self.syntax_success = False - self.operator_success = False - self.programmer_success = False - self.evaluator_success = False - - self.unparsed_queried_components = None - self.queried_components = None - self.queried_model = None - self.queried_function = None - - def _init_prompt_template(self): - self.syntax_reminder_prompt_template = get_prompts("syntax_reminder_prompt") - self.operator_prompt_template = get_prompts("operator_prompt") - - self.code_reminder_prompt_template = get_prompts("code_reminder_prompt") - self.programmer_prompt_template = get_prompts("programmer_prompt") - self.evaluator_prompt_template = get_prompts("evaluator_prompt") - - self.test_prompt_template = get_prompts("test_prompt") - - def _init_fake_team_conversation(self, team_conversation, code_wo_labels): - self.fake_team_conversation = copy.deepcopy(team_conversation) - self.source_code = self.code_reminder_prompt_template.format(source_code=code_wo_labels) - self.fake_team_conversation.append({"agent_name": 'Code reminder', "agent_response": self.source_code}) - - def _init_cnt(self): - self.syntax_cnt = 3 - self.operator_cnt = 3 - self.programmer_cnt = 3 # cnt for programmer output format - self.evaluator_cnt = 3 # cnt for evaluator output format - self.debug_times_left = 3 # cnt for debugging (format correct but not satisfactory code) - self.syntax_success = False - self.operator_success = False - self.programmer_success = False - self.evaluator_success = False - - self.queried_components = None - self.queried_model = None - self.queried_function = None - - def execute_code(self, revision_code, print_code): - #src_code = insert_code(self.source_code, revision_code, 'REVISION') - # src_code = insert_code(src_code, print_code, 'PRINT') - src_code = self.source_code + "\n" + revision_code - execution_rst = run_with_exec(src_code) - - # save the complete code as .py - with open(f"./logs/code_draft/complete_code_{self.debug_times_left}.py", "w") as f: - f.write(src_code) - # save the execution result as .txt - with open(f"./logs/code_draft/execution_result_{self.debug_times_left}.txt", "w") as f: - f.write(execution_rst) - self.fake_team_conversation.append({"agent_name": 'Execution result', "agent_response": execution_rst}) - return src_code, execution_rst - - def tool_call_exp(self, prompt: Optional[str] = None, messages: Optional[List] = None, - seed: int = 10, temperature: float = 0.1, - is_syntax_guidance: bool = False, - syntax_mode: str = 'none'): - - # make sure exactly one of prompt or messages is provided - assert (prompt is None) != (messages is None) - # make sure if messages is provided, it is a list of dicts with role and content - if messages is not None: - assert isinstance(messages, list) - for message in messages: - assert isinstance(message, dict) - assert "role" in message - assert "content" in message - - if not prompt is None: - messages = [ - {"role": "system", "content": self.system_prompt}, - {"role": "user", "content": prompt}, - ] - - if is_syntax_guidance: - tools = self.syntax_guidance_tool - tool_choice = {"type": "function", "function": {"name": "syntax_guidance"}} - else: - if syntax_mode == 'multiple': - tools = self.multiple_tools - elif syntax_mode == 'single': - tools = self.single_tools - elif syntax_mode == 'none': - tools = self.none_tools - elif syntax_mode == 'all': - tools = self.all_tools - else: - raise Exception("Invalid mode!") - tool_choice = "required" - - if type(self.client) in [OpenAI, Client]: - if self.llm not in ["o3"]: - completion = self.client.chat.completions.create( - model=self.llm, - messages=messages, - seed=seed, - temperature=temperature, - tools=tools, - tool_choice=tool_choice - ) - else: - completion = self.client.chat.completions.create( - model=self.llm, - messages=messages, - seed=seed, - tools=tools, - tool_choice=tool_choice - ) - - if completion.choices[0].message.tool_calls: - # internal tool is called - fn_call = completion.choices[0].message.tool_calls[0].function - fn_name = fn_call.name - fn_args = fn_call.arguments - print(f'function name = {fn_name}') - print(f'function arguments = {fn_args}') - else: - raise Exception("No tool call executed by Operator, perhaps because of the 'auto' tool choice!") - else: - raise Exception("Client type not supported!") - return fn_name, fn_args - - def generate_syntax_exp(self, args, messages, team_conversation, models_dict): - while not self.syntax_success and self.syntax_cnt > 0: - component_descriptions = extract_component_descriptions(models_dict) - - if models_dict['model_representation']['model type'] != 'LP': - function_names = [fn for fn in self.function_names if fn != 'sensitivity_analysis'] - else: - function_names = [fn for fn in self.function_names] - - prompt = self.syntax_reminder_prompt_template.format(function_names=function_names, - component_name_meaning_pairs=str(component_descriptions)) - pseudo_messages = self.generate_pseudo_messages(messages, team_conversation, prompt) - self.syntax_cnt -= 1 - try: - syntax_start = time.time() - fn_name, fn_args = self.tool_call_exp(messages=pseudo_messages, - seed=self.syntax_cnt, temperature=args.temperature, - is_syntax_guidance=True) - syntax_end = time.time() - self.syntax_time += (syntax_end - syntax_start) - - self.queried_function = json.loads(fn_args).get("queried_function") - self.queried_components = json.loads(fn_args).get("queried_components") - self.queried_model = json.loads(fn_args).get("queried_model") - # forced syntax_guidance to be called - syntax_output, syntax_mode = syntax_guidance(self.queried_function, - self.queried_components, - self.queried_model, - models_dict) - self.syntax_success = True - return syntax_output, syntax_mode - - except Exception as e: - print(e) - # import traceback - # err = traceback.format_exc() - # print(err) - if self.syntax_cnt == 0: - self.syntax_success = False - return "LLM failed", "none" - - def generate_feedback_exp(self, args, messages, team_conversation, models_dict, syntax_mode): - while not self.operator_success and self.operator_cnt > 0: - prompt = self.operator_prompt_template # nothing to format here - pseudo_messages = self.generate_pseudo_messages(messages, team_conversation, prompt) - self.operator_cnt -= 1 - try: - syntax_start = time.time() - fn_name, fn_args = self.tool_call_exp(messages=pseudo_messages, - seed=self.operator_cnt, temperature=args.temperature, - is_syntax_guidance=False, - syntax_mode=syntax_mode) - syntax_end = time.time() - self.syntax_time += (syntax_end - syntax_start) - - self.queried_function = fn_name - self.queried_model = json.loads(fn_args).get("queried_model") - self.unparsed_queried_components = json.loads(fn_args).get("queried_components") - self.queried_components = fnArgsDecoder(self.unparsed_queried_components) - - # pass the function name and arguments to the function - if fn_name == 'feasibility_restoration': - fn_output = feasibility_restoration(self.queried_components, self.queried_model, models_dict) - elif fn_name == 'sensitivity_analysis': - fn_output = sensitivity_analysis(self.queried_components, self.queried_model, models_dict) - elif fn_name == 'components_retrival': - fn_output = components_retrival(self.queried_components, self.queried_model, models_dict) - elif fn_name == 'evaluate_modification': - fn_output = evaluate_modification(self.queried_components, self.queried_model, models_dict) - else: - raise Exception("invalid function name") - self.operator_success = True - return fn_output - - except Exception as e: - print(e) - import traceback - err = traceback.format_exc() - # embed the error message into the syntax reminder in team_conversation - error_response = f"\n\nProblematic queried_components: {self.unparsed_queried_components} \n\nError: {err}" - team_conversation.append({"agent_name": 'Execution result', "agent_response": error_response}) - - if self.operator_cnt == 0: - self.operator_success = False - return "LLM failed" - - def programmer_loop_exp(self, args, pseudo_messages): - while self.programmer_cnt > 0: - program_start = time.time() - code_output = self.llm_call_exp(messages=pseudo_messages, - seed=self.programmer_cnt, temperature=args.temperature, stream=False) - program_end = time.time() - self.programing_time += (program_end - program_start) - - self.programmer_cnt -= 1 - try: - snippets = re.findall(self.pattern, code_output, flags=re.DOTALL) - # assert len(snippets) <= 2 - # assert snippets[0][0] == 'python' - # assert snippets[1][0] == 'python' - # revision_code = snippets[0][1] - # print_code = snippets[1][1] - revision_code = snippets[0][1] - print_code = "" - self.programmer_success = True - self.fake_team_conversation.append({"agent_name": 'Programmer', "agent_response": code_output}) - return code_output, revision_code, print_code - - except AssertionError as e: - print(e) - # import traceback - # err = traceback.format_exc() - # print(err) - if self.programmer_cnt == 0: - self.programmer_success = False - return None, None, None - - def evaluator_loop_exp(self, args, pseudo_messages): - while self.evaluator_cnt > 0: - evaluation_start = time.time() - # evaluation_output = self.llm_call_exp(messages=pseudo_messages, - # seed=self.evaluator_cnt, temperature=args.temperature, - # json_mode=args.json_mode, stream=False) - evaluation_output = self.llm_call_exp(messages=pseudo_messages, - seed=self.evaluator_cnt, temperature=args.temperature, - json_mode=True, stream=False) - evaluation_end = time.time() - self.evaluation_time += (evaluation_end - evaluation_start) - - self.evaluator_cnt -= 1 - try: - # evaluation = evaluation_output.strip() - # if "```json" in evaluation: - # evaluation = evaluation.split("```json")[1].split("```")[0] - # evaluation = evaluation.replace("\\", "") - # # print('Code review:', evaluation) - # evaluation = json.loads(evaluation) - - # delete until the first '```json' - if "```json" in evaluation_output: - evaluation_output = evaluation_output[evaluation_output.find("```json") + 7:] - evaluation_output = evaluation_output[: evaluation_output.rfind("```")] - start = evaluation_output.find("{") - end = evaluation_output.rfind("}") - evaluation_output = evaluation_output[start:end + 1] - evaluation = json.loads(evaluation_output) - decision = evaluation["decision"] - comment = evaluation["comment"] - - self.evaluator_success = True - self.fake_team_conversation.append({"agent_name": 'Evaluator', "agent_response": evaluation_output}) - return evaluation_output, decision, comment - - except AssertionError as e: - print(e) - # import traceback - # err = traceback.format_exc() - # print(err) - if self.evaluator_cnt == 0: - self.evaluator_success = False - return None, None, None - - def generate_code_exp(self, args, messages, team_conversation, models_dict): - # initialize - self._init_prompt_template() - self._init_fake_team_conversation(team_conversation, models_dict['model_representation']['code']) - - # until the programmer generates the code that evaluator approves - while self.debug_times_left > 0: - # Only init the cnt for programmer and evaluator for every debugging loop - # because _init_cnt() will reset all the success, cnt, debug_times_left - self.programmer_cnt = 2 - self.evaluator_cnt = 2 - # until the programmer generates the code in correct format - programmer_prompt = self.programmer_prompt_template - pseudo_messages = self.generate_pseudo_messages(messages, self.fake_team_conversation, programmer_prompt) - code_output, revision_code, print_code = self.programmer_loop_exp(args, pseudo_messages) - if not self.programmer_success: - return "LLM failed", 'None', 'None' - - # simply executing the code - complete_code, execution_rst = self.execute_code(revision_code, print_code) - - # until the evaluator evaluates the code in correct format - evaluator_prompt = self.evaluator_prompt_template - pseudo_messages = self.generate_pseudo_messages(messages, self.fake_team_conversation, evaluator_prompt) - evaluation_output, decision, comment = self.evaluator_loop_exp(args, pseudo_messages) - if not self.evaluator_success: - return code_output, execution_rst, "LLM failed" - - if decision == 'accept': - return code_output, execution_rst, evaluation_output - else: - self.debug_times_left -= 1 - if self.debug_times_left == 0: - # return the last evaluation output though it is rejected by evaluator - return code_output, execution_rst, evaluation_output - - def generate_report_exp(self, args, messages, team_conversation, models_dict): - self._init_cnt() - - if args.external_experiment: - syntax_output, syntax_mode = 'external_tools', 'none' - self.syntax_success = True - else: - syntax_output, syntax_mode = self.generate_syntax_exp(args, messages, team_conversation, models_dict) - - if not self.syntax_success: - team_conversation.append({"agent_name": 'Syntax reminder', "agent_response": syntax_output}) - messages.append({"role": "assistant", "content": syntax_output}) - else: - if syntax_output != 'external_tools': - # add code reminder to the team_conversation as well to help find correct component indexes - # add syntax reminder - team_conversation.append({"agent_name": 'Code reminder', - "agent_response": models_dict['model_representation']['code']}) - team_conversation.append({"agent_name": 'Syntax reminder', "agent_response": syntax_output}) - function_output = self.generate_feedback_exp(args, messages, team_conversation, models_dict, - syntax_mode) - - team_conversation = [item for item in team_conversation if - item["agent_name"] not in ['Code reminder', 'Syntax reminder']] - - team_conversation.append({"agent_name": 'Operator', "agent_response": function_output}) - if self.operator_success: - messages.append({"role": "assistant", "content": function_output}) - else: - syntax_output = 'external_tools' - - if not args.internal_experiment: - if syntax_output == 'external_tools': - code_output, execution_rst, evaluation_output = self.generate_code_exp(args, messages, - team_conversation, - models_dict) - team_conversation.append({"agent_name": 'Programmer', "agent_response": code_output}) - team_conversation.append({"agent_name": 'Execution result', "agent_response": execution_rst}) - team_conversation.append({"agent_name": 'Evaluator', "agent_response": evaluation_output}) - - messages.append({"role": "assistant", "content": "Programmer:\n\n" + code_output}) - messages.append({"role": "assistant", "content": "Execution result:\n\n" + execution_rst}) - messages.append({"role": "assistant", "content": "Evaluator:\n\n" + evaluation_output}) - - return messages, team_conversation - - def generate_test_result_exp(self, args, messages, gt_a): - self._init_prompt_template() - prompt = self.test_prompt_template.format(human_expert_answer=gt_a) - pseudo_messages = self.generate_pseudo_messages(messages, [], prompt) - pass_or_fail = self.llm_call_exp(messages=pseudo_messages, temperature=args.temperature, stream=False) - return pass_or_fail \ No newline at end of file diff --git a/app.py b/app.py deleted file mode 100644 index 612f17e..0000000 --- a/app.py +++ /dev/null @@ -1,264 +0,0 @@ -import streamlit as st -from openai import OpenAI -import os -from io import StringIO -import time -import tempfile -import io -from extractor import initial_loading -from extractor import update_model_representation, get_skipJSON, feed_skipJSON -from utils import get_agents -from utils import OptiChat_workflow_exp -from pyomo.opt import TerminationCondition -import json - - -def string_generator(long_string, chunk_size=50): - for i in range(0, len(long_string), chunk_size): - yield long_string[i:i+chunk_size] - time.sleep(0.1) # Optionally add a small delay between each yield - - -client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) -st.session_state['client'] = client -st.session_state['temperature'] = 0.1 # by default -st.session_state['json_mode'] = True # by default -st.session_state['illustration_stream'] = True # by default -st.session_state['inference_stream'] = True # by default -st.session_state['explanation_stream'] = True # by default -st.session_state['internal_experiment'] = False # by default -st.session_state['external_experiment'] = False # by default - -st.set_page_config(layout='wide') - -st.title("OptiChat: Talk to your Optimization Model") - - -gpt_model = st.sidebar.selectbox(label="GPT-Model", options=["gpt-4-turbo-preview", "gpt-4-turbo", "gpt-4-1106-preview", "gpt-4", "gpt-3.5-turbo", "gpt-3.5-turbo-16k"], ) -st.session_state["gpt_model"] = gpt_model -# Set a default model -if "gpt_model" not in st.session_state: - st.session_state["gpt_model"] = "gpt-4-turbo-preview" -if "models_dict" not in st.session_state: - st.session_state["models_dict"] = {"model_representation": {}} -if "code" not in st.session_state: - st.session_state["code"] = "" - -st.sidebar.subheader("Load Pyomo File") -uploaded_file = st.sidebar.file_uploader("Upload Model", type=["py"]) -uploaded_json = st.sidebar.file_uploader("Upload JSON", type=["json"]) -st.session_state['py_path'] = None -st.session_state['fn_names'] = ["feasibility_restoration", - "sensitivity_analysis", - "components_retrival", - "evaluate_modification", - "external_tools"] - -interpreter, explainer, engineer, coordinator = get_agents(st.session_state.fn_names, - st.session_state.client, - st.session_state.gpt_model) -st.session_state['Interpreter'] = interpreter -st.session_state['Explainer'] = explainer -st.session_state['Engineer'] = engineer -st.session_state['Coordinator'] = coordinator - - -if not st.session_state.get("messages"): - st.session_state["messages"] = [] - -if not st.session_state.get("team_conversation"): - st.session_state["team_conversation"] = [] - -if not st.session_state.get("chat_history"): - st.session_state["chat_history"] = [] - -if not st.session_state.get("detailed_chat_history"): - st.session_state["detailed_chat_history"] = [] - - -def process(): - if uploaded_file is None: - st.error("Please upload your model first.") - return - - models_dict, code = initial_loading(uploaded_file) - - with st.chat_message("user"): - st.markdown("I have uploaded a Pyomo model.") - st.session_state.messages.append({"role": "user", "content": "I have uploaded a Pyomo model."}) - # interpret the model components - models_dict, cnt, completion = st.session_state.Interpreter.generate_interpretation_exp(st.session_state, - models_dict, code) - st.session_state['models_dict'] = models_dict - st.session_state['code'] = code - # update model representation with component descriptions - update_model_representation(st.session_state.models_dict) - # illustrate the model - illustration_stream = st.session_state.Interpreter.generate_illustration_exp(st.session_state, - models_dict["model_representation"]) - with st.chat_message("assistant"): - illustration = st.write_stream(illustration_stream) - # update model representation with model description - st.session_state.models_dict['model_1']['model description'] = illustration - update_model_representation(st.session_state.models_dict) - # if the model is infeasible, generate inference - if st.session_state.models_dict['model_1']['model status'] in [TerminationCondition.infeasible, - TerminationCondition.infeasibleOrUnbounded]: - inference_stream = st.session_state.Interpreter.generate_inference_exp(st.session_state, - st.session_state.models_dict["model_representation"]) - with st.chat_message("assistant"): - inference = st.write_stream(inference_stream) - # update model representation with inference description - st.session_state.models_dict['model_1']['model description'] = illustration + '\n' + inference - update_model_representation(st.session_state.models_dict) - - # append model representation to messages - st.session_state.messages.append({"role": "assistant", - "content": st.session_state.models_dict["model_representation"]["model description"]}) - - # append detailed chat history - st.session_state.chat_history.append("user: I have uploaded a Pyomo model.") - st.session_state.chat_history.append("assistant: " + - st.session_state.models_dict["model_representation"]["model description"]) - st.session_state.detailed_chat_history.append("user: I have uploaded a Pyomo model.") - st.session_state.detailed_chat_history.append("assistant: " + - st.session_state.models_dict["model_representation"]["model description"]) - - # save model_description and description of every component - if not os.path.exists("logs/model_json"): - os.makedirs("logs/model_json") - if not os.path.exists("logs/code_draft"): - os.makedirs("logs/code_draft") - if not os.path.exists("logs/ilps"): - os.makedirs("logs/ilps") - - json2save = get_skipJSON(st.session_state.models_dict["model_representation"]) - with open(f"logs/model_json/{os.path.splitext(uploaded_file.name)[0]}.json", "w") as f: - json.dump(json2save, f) - - -def load_json(): - if uploaded_file is None: - st.error("Please upload your model first.") - return - if uploaded_json is None: - st.error("Please upload your json file first.") - return - - models_dict, code = initial_loading(uploaded_file) - - with st.chat_message("user"): - st.markdown("I have uploaded a Pyomo model.") - st.session_state.messages.append({"role": "user", "content": "I have uploaded a Pyomo model."}) - - skipJSON = json.load(uploaded_json) - models_dict = feed_skipJSON(skipJSON, models_dict) - - st.session_state["models_dict"] = models_dict - st.session_state['code'] = code - # update model representation with component and model descriptions - update_model_representation(st.session_state.models_dict) - - time.sleep(8) - stream = string_generator(skipJSON["model description"]) - with st.chat_message("assistant"): - st.write_stream(stream) - - # append model representation to messages - st.session_state.messages.append({"role": "assistant", - "content": st.session_state.models_dict["model_representation"][ - "model description"]}) - - # append detailed chat history - st.session_state.chat_history.append("user: I have uploaded a Pyomo model.") - st.session_state.chat_history.append("assistant: " + - st.session_state.models_dict["model_representation"]["model description"]) - st.session_state.detailed_chat_history.append("user: I have uploaded a Pyomo model.") - st.session_state.detailed_chat_history.append("assistant: " + - st.session_state.models_dict["model_representation"][ - "model description"]) - - -chat_history_texts = '\n\n'.join(st.session_state.chat_history) -detailed_chat_history_texts = '\n\n'.join(st.session_state.detailed_chat_history) - -st.sidebar.button("Process", on_click=process) -st.sidebar.button("Load JSON", on_click=load_json) - - -show_model_representation = st.sidebar.checkbox("Show Model Representation", False) -model_representation_placeholder = st.empty() -show_code = st.sidebar.checkbox("Show Code", False) -code_placeholder = st.empty() -show_tech_feedback = st.sidebar.checkbox("Show Technical Feedback", False) -tech_feedback_placeholder = st.empty() - -st.sidebar.download_button(label="Export Chat History", data=chat_history_texts, - file_name='chat_history.txt', mime='text/plain') -st.sidebar.download_button(label="Export Detailed Chat History", data=detailed_chat_history_texts, - file_name='detailed_chat_history.txt', mime='text/plain') - - -st.sidebar.markdown("### Status") -status = st.sidebar.empty() - -st.sidebar.markdown("### Round") -cur_round = st.sidebar.empty() - -st.sidebar.markdown("### Agent") -agent_name = st.sidebar.empty() - -st.sidebar.markdown("### Task") -task = st.sidebar.empty() - - -if show_model_representation: - with model_representation_placeholder.container(): - st.json(st.session_state.models_dict["model_representation"]) -else: - model_representation_placeholder.empty() - -if show_code: - with code_placeholder.container(): - st.code(st.session_state.code) -else: - code_placeholder.empty() - -if show_tech_feedback: - with tech_feedback_placeholder.container(): - for message in st.session_state.team_conversation: - st.write(message['agent_name'] + ': ' + message['agent_response']) - # if message['agent_name'] in ['Programmer', 'Operator', 'Syntax reminder', 'Explainer', 'Coordinator']: - # st.write(message['agent_name'] + ': ' + message['agent_response']) -else: - tech_feedback_placeholder.empty() - - -# Display chat messages from history on app rerun -for message in st.session_state.messages: - with st.chat_message(message["role"]): - st.markdown(message["content"]) - -# Accept user input -if prompt := st.chat_input("Enter your query here..."): - st.session_state.messages.append({"role": "user", "content": prompt}) - with st.chat_message("user"): - st.markdown(prompt) - - updated_messages, team_conversation = OptiChat_workflow_exp(st.session_state, - st.session_state.Coordinator, - st.session_state.Engineer, - st.session_state.Explainer, - st.session_state.messages, - st.session_state.models_dict) - print('OptiChat_out:', updated_messages) - st.session_state.messages = updated_messages - - # # update detailed chat history - # st.session_state.chat_history.append("user: " + prompt) - # st.session_state.chat_history.append("assistant: " + OptiChat_out) - - # st.session_state.detailed_chat_history.append("user: " + prompt) - # for message in st.session_state.team_conversation: - # st.session_state.detailed_chat_history.append(f"***{message['agent_name']}***: {message['agent_response']}") - # st.session_state.detailed_chat_history.append("assistant: " + OptiChat_out) diff --git a/cfg_debug_use.json b/cfg_debug_use.json new file mode 100644 index 0000000..439ec3b --- /dev/null +++ b/cfg_debug_use.json @@ -0,0 +1,14 @@ +{ "model_name": "debug", + "models": { + "local_resources": [ + "my_model.pkl" + ], + "is_solved": false, + "is_lp": false + }, + "models_code": { + "local_resources": [ + "Feas/aircraft.py" + ] + } +} \ No newline at end of file diff --git a/debug_use/allocation_model_infeasible.py b/debug_use/allocation_model_infeasible.py new file mode 100644 index 0000000..5906aa2 --- /dev/null +++ b/debug_use/allocation_model_infeasible.py @@ -0,0 +1,365 @@ +import pyomo.environ as pyo +import numpy as np + +## Data + +data = { + 'S': ['DC1', 'DC2', 'DC3'], # Distribution Centers + 'R': ['Store_A', 'Store_B', 'Store_C', 'Store_D'], # Retail Stores + 'T': [0, 1, 2, 3, 4, 5, 6], # Week 0 through Week 6 (7 weeks) + + 'c': { # Transportation cost per unit (based on distance) + # DC1 is closest to Store_A and Store_B + ('DC1', 'Store_A'): 5, ('DC1', 'Store_B'): 7, ('DC1', 'Store_C'): 15, ('DC1', 'Store_D'): 18, + # DC2 is in the middle + ('DC2', 'Store_A'): 12, ('DC2', 'Store_B'): 10, ('DC2', 'Store_C'): 8, ('DC2', 'Store_D'): 11, + # DC3 is closest to Store_C and Store_D + ('DC3', 'Store_A'): 20, ('DC3', 'Store_B'): 16, ('DC3', 'Store_C'): 6, ('DC3', 'Store_D'): 5, + }, + + # Initial inventories - DCs have good stock, stores are at different levels + 's0_i': {'DC1': 450, 'DC2': 380, 'DC3': 420}, # Total: 1250 units available + 's0_j': {'Store_A': 35, 'Store_B': 22, 'Store_C': 18, 'Store_D': 28}, # Stores starting at different levels + + # Need calculation: RUTL - Net_Inventory (only when NI < RUTL) + # Stores have different capacity needs based on size and sales volume + 'need': { + # Store_A: Large store, high capacity needs + ('Store_A', 0): 45, ('Store_A', 1): 38, ('Store_A', 2): 42, ('Store_A', 3): 35, + ('Store_A', 4): 40, ('Store_A', 5): 48, ('Store_A', 6): 45, + + # Store_B: Medium store, moderate needs + ('Store_B', 0): 38, ('Store_B', 1): 35, ('Store_B', 2): 32, ('Store_B', 3): 40, + ('Store_B', 4): 38, ('Store_B', 5): 42, ('Store_B', 6): 36, + + # Store_C: Small store, lower needs but growing + ('Store_C', 0): 27, ('Store_C', 1): 30, ('Store_C', 2): 28, ('Store_C', 3): 32, + ('Store_C', 4): 35, ('Store_C', 5): 38, ('Store_C', 6): 40, + + # Store_D: Medium-large store, consistent needs + ('Store_D', 0): 32, ('Store_D', 1): 35, ('Store_D', 2): 38, ('Store_D', 3): 36, + ('Store_D', 4): 40, ('Store_D', 5): 42, ('Store_D', 6): 38, + }, + + # Aim: Normalized need based on priority and fairness + # Total need at t=0: 45+38+27+32 = 142 + # Available capacity ~1250, but we allocate conservatively + # Store_A gets 90% of need (high priority, flagship store) + # Store_B gets 85% of need (standard priority) + # Store_C gets 75% of need (newer, smaller store) + # Store_D gets 80% of need (standard priority) + 'aim': { + # Store_A: 90% of need (flagship store priority) + ('Store_A', 0): 40, ('Store_A', 1): 34, ('Store_A', 2): 38, ('Store_A', 3): 32, + ('Store_A', 4): 36, ('Store_A', 5): 43, ('Store_A', 6): 40, + + # Store_B: 85% of need + ('Store_B', 0): 32, ('Store_B', 1): 30, ('Store_B', 2): 27, ('Store_B', 3): 34, + ('Store_B', 4): 32, ('Store_B', 5): 36, ('Store_B', 6): 31, + + # Store_C: 75% of need (lower priority, building up gradually) + ('Store_C', 0): 20, ('Store_C', 1): 23, ('Store_C', 2): 21, ('Store_C', 3): 24, + ('Store_C', 4): 26, ('Store_C', 5): 29, ('Store_C', 6): 30, + + # Store_D: 80% of need + ('Store_D', 0): 26, ('Store_D', 1): 28, ('Store_D', 2): 30, ('Store_D', 3): 29, + ('Store_D', 4): 32, ('Store_D', 5): 34, ('Store_D', 6): 30, + }, + + 'M': 10000, # Big M value + + # Reorder points: Based on lead time demand + safety stock + # RP = (avg weekly demand * max lead time) + safety stock + 'RP': { + ('Store_A', 0): 25, ('Store_A', 1): 25, ('Store_A', 2): 25, ('Store_A', 3): 25, + ('Store_A', 4): 25, ('Store_A', 5): 25, ('Store_A', 6): 25, + + ('Store_B', 0): 20, ('Store_B', 1): 20, ('Store_B', 2): 20, ('Store_B', 3): 20, + ('Store_B', 4): 20, ('Store_B', 5): 20, ('Store_B', 6): 20, + + ('Store_C', 0): 15, ('Store_C', 1): 15, ('Store_C', 2): 15, ('Store_C', 3): 15, + ('Store_C', 4): 15, ('Store_C', 5): 15, ('Store_C', 6): 15, + + ('Store_D', 0): 22, ('Store_D', 1): 22, ('Store_D', 2): 22, ('Store_D', 3): 22, + ('Store_D', 4): 22, ('Store_D', 5): 22, ('Store_D', 6): 22, + }, + + # Reorder up to levels: Maximum inventory capacity + 'RUTL': { + ('Store_A', 0): 80, ('Store_A', 1): 80, ('Store_A', 2): 80, ('Store_A', 3): 80, + ('Store_A', 4): 80, ('Store_A', 5): 80, ('Store_A', 6): 80, + + ('Store_B', 0): 60, ('Store_B', 1): 60, ('Store_B', 2): 60, ('Store_B', 3): 60, + ('Store_B', 4): 60, ('Store_B', 5): 60, ('Store_B', 6): 60, + + ('Store_C', 0): 45, ('Store_C', 1): 45, ('Store_C', 2): 45, ('Store_C', 3): 45, + ('Store_C', 4): 45, ('Store_C', 5): 45, ('Store_C', 6): 45, + + ('Store_D', 0): 60, ('Store_D', 1): 60, ('Store_D', 2): 60, ('Store_D', 3): 60, + ('Store_D', 4): 60, ('Store_D', 5): 60, ('Store_D', 6): 60, + }, + + # Demand: Actual customer demand (slightly seasonal pattern) + # Week 5-6 show increased demand (weekend/promotion effect) + 'd': { + # Store_A: High volume store + ('Store_A', 0): 18, ('Store_A', 1): 20, ('Store_A', 2): 19, ('Store_A', 3): 21, + ('Store_A', 4): 22, ('Store_A', 5): 28, ('Store_A', 6): 26, + + # Store_B: Medium volume + ('Store_B', 0): 15, ('Store_B', 1): 16, ('Store_B', 2): 14, ('Store_B', 3): 17, + ('Store_B', 4): 18, ('Store_B', 5): 22, ('Store_B', 6): 20, + + # Store_C: Lower volume, growing + ('Store_C', 0): 10, ('Store_C', 1): 11, ('Store_C', 2): 12, ('Store_C', 3): 13, + ('Store_C', 4): 14, ('Store_C', 5): 16, ('Store_C', 6): 15, + + # Store_D: Medium-high volume + ('Store_D', 0): 16, ('Store_D', 1): 17, ('Store_D', 2): 18, ('Store_D', 3): 17, + ('Store_D', 4): 19, ('Store_D', 5): 24, ('Store_D', 6): 21, + }, + + # Lead times: Varies by distance + # DC1 to nearby stores: 1 week, far stores: 2 weeks + # DC2: mostly 1 week (centrally located) + # DC3 to nearby stores: 1 week, far stores: 2 weeks + 'LT': { + # DC1 lead times + ('DC1', 'Store_A', 0): 1, ('DC1', 'Store_A', 1): 1, ('DC1', 'Store_A', 2): 1, + ('DC1', 'Store_A', 3): 1, ('DC1', 'Store_A', 4): 1, ('DC1', 'Store_A', 5): 1, ('DC1', 'Store_A', 6): 1, + + ('DC1', 'Store_B', 0): 1, ('DC1', 'Store_B', 1): 1, ('DC1', 'Store_B', 2): 1, + ('DC1', 'Store_B', 3): 1, ('DC1', 'Store_B', 4): 1, ('DC1', 'Store_B', 5): 1, ('DC1', 'Store_B', 6): 1, + + ('DC1', 'Store_C', 0): 2, ('DC1', 'Store_C', 1): 2, ('DC1', 'Store_C', 2): 2, + ('DC1', 'Store_C', 3): 2, ('DC1', 'Store_C', 4): 2, ('DC1', 'Store_C', 5): 2, ('DC1', 'Store_C', 6): 2, + + ('DC1', 'Store_D', 0): 2, ('DC1', 'Store_D', 1): 2, ('DC1', 'Store_D', 2): 2, + ('DC1', 'Store_D', 3): 2, ('DC1', 'Store_D', 4): 2, ('DC1', 'Store_D', 5): 2, ('DC1', 'Store_D', 6): 2, + + # DC2 lead times (centrally located - all 1 week) + ('DC2', 'Store_A', 0): 1, ('DC2', 'Store_A', 1): 1, ('DC2', 'Store_A', 2): 1, + ('DC2', 'Store_A', 3): 1, ('DC2', 'Store_A', 4): 1, ('DC2', 'Store_A', 5): 1, ('DC2', 'Store_A', 6): 1, + + ('DC2', 'Store_B', 0): 1, ('DC2', 'Store_B', 1): 1, ('DC2', 'Store_B', 2): 1, + ('DC2', 'Store_B', 3): 1, ('DC2', 'Store_B', 4): 1, ('DC2', 'Store_B', 5): 1, ('DC2', 'Store_B', 6): 1, + + ('DC2', 'Store_C', 0): 1, ('DC2', 'Store_C', 1): 1, ('DC2', 'Store_C', 2): 1, + ('DC2', 'Store_C', 3): 1, ('DC2', 'Store_C', 4): 1, ('DC2', 'Store_C', 5): 1, ('DC2', 'Store_C', 6): 1, + + ('DC2', 'Store_D', 0): 1, ('DC2', 'Store_D', 1): 1, ('DC2', 'Store_D', 2): 1, + ('DC2', 'Store_D', 3): 1, ('DC2', 'Store_D', 4): 1, ('DC2', 'Store_D', 5): 1, ('DC2', 'Store_D', 6): 1, + + # DC3 lead times + ('DC3', 'Store_A', 0): 2, ('DC3', 'Store_A', 1): 2, ('DC3', 'Store_A', 2): 2, + ('DC3', 'Store_A', 3): 2, ('DC3', 'Store_A', 4): 2, ('DC3', 'Store_A', 5): 2, ('DC3', 'Store_A', 6): 2, + + ('DC3', 'Store_B', 0): 2, ('DC3', 'Store_B', 1): 2, ('DC3', 'Store_B', 2): 2, + ('DC3', 'Store_B', 3): 2, ('DC3', 'Store_B', 4): 2, ('DC3', 'Store_B', 5): 2, ('DC3', 'Store_B', 6): 2, + + ('DC3', 'Store_C', 0): 1, ('DC3', 'Store_C', 1): 1, ('DC3', 'Store_C', 2): 1, + ('DC3', 'Store_C', 3): 1, ('DC3', 'Store_C', 4): 1, ('DC3', 'Store_C', 5): 1, ('DC3', 'Store_C', 6): 1, + + ('DC3', 'Store_D', 0): 1, ('DC3', 'Store_D', 1): 1, ('DC3', 'Store_D', 2): 1, + ('DC3', 'Store_D', 3): 1, ('DC3', 'Store_D', 4): 1, ('DC3', 'Store_D', 5): 1, ('DC3', 'Store_D', 6): 1, + }, + + # Initial net inventory (on-hand + in-transit) + 'NI_init': { + 'Store_A': 35, # Same as s0_j if no in-transit + 'Store_B': 22, + 'Store_C': 18, + 'Store_D': 28, + }, + + 'incoming_q': {}, # No external incoming quantities in this scenario, + + 'inventory_limit': { + 'DC1': 150, # Capacity limit + 'DC2': 350, + 'DC3': 380, + 'Store_A': 70, + 'Store_B': 50, + 'Store_C': 40, + 'Store_D': 55, + } +} + + +# Building model + +""" + Create the Fair Allocation Model as specified in the PDF. + + Parameters: + ----------- + data : dict + Dictionary containing all required data: + - S: set of senders + - R: set of recipients + - T: set of time periods {t0, ..., tk} + - c: cost matrix c[i,j] + - s0_i: initial inventory for senders + - s0_j: initial inventory for recipients + - need: need[j,t] + - aim: aim[j,t] + - M: big M value + - NI_init: initial net inventory NI[j,t0] + - RP: reorder point RP[j,t] + - RUTL: reorder up to level RUTL[j,t] + - d: demand/sales d[j,t] + - LT: lead time LT[i,j,t] + - incoming_q: incoming quantities +""" + +model = pyo.ConcreteModel() + +# Sets +model.S = pyo.Set(initialize=data['S'], doc = "Set of all Senders/Suppliers") # Set of senders +model.R = pyo.Set(initialize=data['R'], doc = "Set of all Recipients") # Set of recipients +model.T = pyo.Set(initialize=data['T'], doc = "Set of all time periods") # Time periods +model.T_plus = pyo.Set(initialize=data['T'][1:], doc = "Set of all time periods except 0") # T excluding t0 + +# Parameters +model.c = pyo.Param(model.S, model.R, initialize=data['c'], doc = "Cost of item to go from i to j", mutable = True) # Cost matrix +model.s0_i = pyo.Param(model.S, initialize=data['s0_i'], doc = "Initial inventory of senders", mutable = True) # Initial sender inventory +model.s0_j = pyo.Param(model.R, initialize=data['s0_j'], doc = "Initial inventory of receipients", mutable = True) # Initial recipient inventory +model.need = pyo.Param(model.R, model.T, initialize=data['need'], doc = "Need at location j at time t", mutable = True) # Need at location j, time t +model.aim = pyo.Param(model.R, model.T, initialize=data['aim'], doc = " Aimed quantity at receiver j at time t", mutable = True) # Aimed quantity +model.M = pyo.Param(initialize=data['M'], mutable = True) # Big M +model.RP = pyo.Param(model.R, model.T, initialize=data['RP'], doc = "Reoder Point", mutable = True) # Reorder point +model.RUTL = pyo.Param(model.R, model.T, initialize=data['RUTL'], doc = "Reorder up to level", mutable = True) # Reorder up to level +model.d = pyo.Param(model.R, model.T, initialize=data['d'], doc = "Demand at receiver j at time t", mutable = True) # Demand/sales +model.LT = pyo.Param(model.S, model.R, model.T, initialize=data['LT'],doc = "Lead time from supplier i to receiver j at time t", mutable = True) # Lead time +model.need_min_param = pyo.Param(initialize = 10, doc = "limit for need quantites", mutable = True) + +# Decision Variables +model.U_plus = pyo.Var(within=pyo.NonNegativeReals) # U+ +model.U_minus = pyo.Var(within=pyo.NonNegativeReals) # U- +model.q = pyo.Var(model.S, model.R, model.T, within=pyo.NonNegativeReals, doc = "Quantity ordered from location i to j at time t") # Quantity from i to j at t +model.x = pyo.Var(model.S, model.R, model.T, within=pyo.Binary, doc = "Binary: 1 if shipping from i to j at t") # Binary: 1 if shipping from i to j at t +model.s = pyo.Var(pyo.Set(initialize=model.S | model.R), model.T, within=pyo.NonNegativeReals, doc = "On-hand inventory at location i at time t") # Inventory at location i/j, time t +model.NI = pyo.Var(model.R, model.T, within=pyo.NonNegativeReals, doc = "Net inventory at receiver j at time t") # Net inventory at j, time t +model.sls = pyo.Var(model.R, model.T, within=pyo.NonNegativeReals, doc = "Expected Sales at j at time t") # Expected sales at j, time t + +# Objective Function +def objective_rule(model): + return (model.U_plus + model.U_minus + + sum(model.c[i,j] * model.x[i,j,t] for i in model.S for j in model.R for t in model.T) - + 2 * sum(model.q[i,j,t] for i in model.S for j in model.R for t in model.T)) +model.obj = pyo.Objective(rule=objective_rule, sense=pyo.minimize) + +# Constraints + +def need_bound_constraint(model, r, t): + return model.need[r, t] <= model.need_min_param + +model.need_min = pyo.Constraint(model.R, model.T, rule = need_bound_constraint, doc = "Need bounded my minimum need") + +# Sender capacity constraint +def sender_capacity_rule(model, i, t): + return sum(model.q[i,j,t] for j in model.R) <= model.s[i,t] +model.sender_capacity = pyo.Constraint(model.S, model.T, rule=sender_capacity_rule, doc = "Sender capacity constraint: sum over j,t of q[i,j,t] <= s[i,t] for all i in S, t") + +# Recipient need constraint +def recipient_need_rule(model, j, t): + return sum(model.q[i,j,t] for i in model.S) <= model.need[j,t] +model.recipient_need = pyo.Constraint(model.R, model.T, rule=recipient_need_rule, doc = "Recipient need constraint: sum over i of q[i,j,t] <= need[j,t] for all j in R, t") + +# Linking constraint +def linking_rule(model, i, j, t): + return model.q[i,j,t] <= model.need[j,t] * model.x[i,j,t] +model.linking = pyo.Constraint(model.S, model.R, model.T, rule=linking_rule, doc = "Linking constraint: q[i,j,t] <= need[j,t] * x[i,j,t] for all i,j,t") + +# U_minus constraint +def u_minus_rule(model, j, t): + return model.U_minus >= model.aim[j,t] - sum(model.q[i,j,t] for i in model.S) +model.u_minus_constraint = pyo.Constraint(model.R, model.T, rule=u_minus_rule, doc = " U_minus constraint: U_minus >= aim[j,t] - sum over i of q[i,j,t] for all j in R, t") + +# U_plus constraint +def u_plus_rule(model, j, t): + return model.U_plus >= sum(model.q[i,j,t] for i in model.S) - model.aim[j,t] +model.u_plus_constraint = pyo.Constraint(model.R, model.T, rule=u_plus_rule, doc = "U_plus constraint: U_plus >= sum over i of q[i,j,t] - aim[j,t] for all j in R, t") + +# Initial sender inventory +def init_sender_inventory_rule(model, i): + return model.s[i,0] == model.s0_i[i] +model.init_sender_inventory = pyo.Constraint(model.S, rule=init_sender_inventory_rule, doc = "Initial sender inventory: s[i,t0] = s0_i for all i in S") + +# Initial recipient inventory +def init_recipient_inventory_rule(model, j): + return model.s[j,0] == model.s0_j[j] +model.init_recipient_inventory = pyo.Constraint(model.R, rule=init_recipient_inventory_rule, doc = "Initial recipient inventory: s[j,t0] = s0_j for all j in R") + +# Sender inventory update +def sender_inventory_update_rule(model, i, t): + if t < data["T"][-1]: + return model.s[i,t + 1] == model.s[i,t] - sum(model.q[i,j,t] for j in model.R) + else: + return pyo.Constraint.Skip +model.sender_inventory_update = pyo.Constraint(model.S, model.T, rule=sender_inventory_update_rule, doc = "Sender inventory update: s[i,t+1] = s[i,t] - sum over j of q[i,j,t] for all i in S, t") + +# Big M constraint +def big_m_rule(model, j, t): + return model.M * sum(model.x[i,j,t] for i in model.S) >= model.NI[j,t] - model.RP[j,t] +model.big_m_constraint = pyo.Constraint(model.R, model.T, rule=big_m_rule, doc = "Big M constraint: (M * sum over i of x[i,j,t]) >= NI[j,t] - RP[j,t] for all j in R, t") + +# RUTL constraint +def rutl_rule(model, j, t): + return model.s[j,t] + sum(model.q[i,j,t] for i in model.S) <= model.RUTL[j,t] +model.rutl_constraint = pyo.Constraint(model.R, model.T, rule=rutl_rule, doc = "RUTL constraint: s[j,t] + sum over i of q[i,j,t] <= RUTL[j,t] for all j in R, t") + +# Sales constraint +def sales_rule(model, j, t): + return model.sls[j,t] <= model.s[j,t] +model.sales_upper1 = pyo.Constraint(model.R, model.T, rule=sales_rule, doc = "Sales constraint: sls[j,t] = s[j,t] for all j in R, t") + +def sales_rule2(model, j, t): + return model.sls[j,t] <= model.d[j,t] +model.sales_upper2 = pyo.Constraint(model.R, model.T, rule=sales_rule2, doc = "Sales constraint: sls[j,t] <= d[j,t] for all j in R, t") + +def recipient_inventory_update_rule(model, j, t): + if t == model.T[-1]: + return pyo.Constraint.Skip + + # Calculate incoming quantities considering lead times + incoming = sum( + model.q[i, j, t - pyo.value(model.LT[i, j, t])] + for i in model.S + if t - pyo.value(model.LT[i, j, t]) in model.T + ) + + return model.s[j, t + 1] == model.s[j, t] - model.sls[j, t] + incoming + +model.recipient_inventory_update = pyo.Constraint(model.R, model.T, rule=recipient_inventory_update_rule, doc = "s[j,t+1] = s[j,t] - sls[j,t] + sum over i of q[i,j,t-LT[i,j,t]]") + +# Net inventory update: Complex constraint +def net_inventory_update_rule(model, j, t): + if t == model.T[-1]: + return pyo.Constraint.Skip + + # Start with current inventory + ni_value = model.s[j, t] + + # Add in-transit quantities: Sum over i, sum over u in (t-LT[i,j,t], t) + for i in model.S: + lt = pyo.value(model.LT[i, j, t]) + # Sum over u in (t-LT[i,j,t], t] - note: open interval on left, closed on right + for u in range(t - lt + 1, t + 1): + if u in model.T: + ni_value += model.q[i, j, u] + + return model.NI[j, t + 1] == ni_value + +model.net_inventory_update = pyo.Constraint(model.R, model.T, rule=net_inventory_update_rule, doc = "NI[j,t+1] = s[j,t] + sum over i sum over u in (t-LT[i,j,t], t) of q[i,j,u]") + +def inventory_capacity_rule(model, i, t): + return model.s[i, t] <= data['inventory_limit'][i] + +model.inventory_capacity = pyo.Constraint( + pyo.Set(initialize=model.S | model.R), + model.T, + rule=inventory_capacity_rule, doc = "Inventory Capacity Constraint" +) + + diff --git a/debug_use/allocation_model_infeasible_description.txt b/debug_use/allocation_model_infeasible_description.txt new file mode 100644 index 0000000..cb7ec82 --- /dev/null +++ b/debug_use/allocation_model_infeasible_description.txt @@ -0,0 +1,29 @@ +Introduction to the Optimization Model +This optimization model is designed for a supply chain management scenario, specifically for a retail distribution network. It involves managing the distribution of goods from several distribution centers (DCs) to multiple retail stores over a series of time periods (weeks). The primary users of this model are supply chain managers and logistics coordinators who aim to optimize the flow of goods while minimizing costs and ensuring that each store's needs are met as closely as possible. +The model's goal is to determine the optimal quantities of goods to be shipped from each distribution center to each store, each week, in a way that balances cost efficiency with the need to meet store demands and maintain inventory levels within specified limits. +Decisions (Variables) +The model includes several decision variables: +Quantity Ordered (q[i,j,t]): The amount of goods to be shipped from distribution center i to store j in week t. +Shipping Decision (x[i,j,t]): A binary variable that indicates whether shipping occurs from DC i to store j in week t. +Inventory Levels (s[i,t]): The inventory level at location i (which could be a DC or a store) at the end of week t. +Net Inventory (NI[j,t]): The net inventory at store j at the end of week t, considering both on-hand and in-transit goods. +Expected Sales (sls[j,t]): The expected sales at store j in week t. +Surplus (U_plus) and Shortfall (U_minus): Variables representing the total amount by which the actual shipments exceed or fall short of the targeted levels across all stores and time periods. +Data or Information (Parameters) +The model uses various parameters: +Transportation Costs (c[i,j]): Costs associated with shipping goods from DC i to store j. +Initial Inventories (s0_i for DCs, s0_j for stores): Starting inventory levels at each location. +Store Needs (need[j,t]) and Aimed Quantities (aim[j,t]): The required and targeted inventory levels for each store j in each week t. +Reorder Points (RP[j,t]) and Reorder Up To Levels (RUTL[j,t]): Inventory thresholds that trigger reordering and the maximum inventory levels, respectively. +Demand/Sales (d[j,t]): Actual customer demand at store j in week t. +Lead Times (LT[i,j,t]): Time taken for shipments to go from DC i to store j. +Constraints +The model's constraints ensure that: +Shipments do not exceed the sender's capacity or the recipient's need. +Inventory levels do not exceed capacity limits at any location. +The net inventory at each store is maintained above the reorder point but below the maximum level. +The actual shipments align with the binary shipping decisions. +Sales do not exceed the available inventory or the expected demand. +Objective +The objective of the model is to minimize the total cost of transportation, the surplus (U_plus), and the shortfall (U_minus) in supply, while also subtracting a value proportional to the total quantity shipped. This function aims to balance cost minimization with the goal of meeting supply targets as accurately as possible. +By optimizing these variables within the given constraints, the model helps supply chain managers efficiently allocate resources, minimize costs, and ensure that each store's inventory levels are adequate to meet anticipated sales demands. \ No newline at end of file diff --git a/debug_use/allocation_model_infeasible_unsolved.pkl b/debug_use/allocation_model_infeasible_unsolved.pkl new file mode 100644 index 0000000..ec9cee6 Binary files /dev/null and b/debug_use/allocation_model_infeasible_unsolved.pkl differ diff --git a/debug_use/allocation_model_infeasible_unsolved.txt b/debug_use/allocation_model_infeasible_unsolved.txt new file mode 100644 index 0000000..fb88157 --- /dev/null +++ b/debug_use/allocation_model_infeasible_unsolved.txt @@ -0,0 +1,1100 @@ +4 Set Declarations + R : Set of all Recipients + Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 4 : {'Store_A', 'Store_B', 'Store_C', 'Store_D'} + S : Set of all Senders/Suppliers + Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'DC1', 'DC2', 'DC3'} + T : Set of all time periods + Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 7 : {0, 1, 2, 3, 4, 5, 6} + T_plus : Set of all time periods except 0 + Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 6 : {1, 2, 3, 4, 5, 6} + +11 Param Declarations + LT : Lead time from supplier i to receiver j at time t + Size=84, Index=S*R*T, Domain=Any, Default=None, Mutable=True + Key : Value + ('DC1', 'Store_A', 0) : 1 + ('DC1', 'Store_A', 1) : 1 + ('DC1', 'Store_A', 2) : 1 + ('DC1', 'Store_A', 3) : 1 + ('DC1', 'Store_A', 4) : 1 + ('DC1', 'Store_A', 5) : 1 + ('DC1', 'Store_A', 6) : 1 + ('DC1', 'Store_B', 0) : 1 + ('DC1', 'Store_B', 1) : 1 + ('DC1', 'Store_B', 2) : 1 + ('DC1', 'Store_B', 3) : 1 + ('DC1', 'Store_B', 4) : 1 + ('DC1', 'Store_B', 5) : 1 + ('DC1', 'Store_B', 6) : 1 + ('DC1', 'Store_C', 0) : 2 + ('DC1', 'Store_C', 1) : 2 + ('DC1', 'Store_C', 2) : 2 + ('DC1', 'Store_C', 3) : 2 + ('DC1', 'Store_C', 4) : 2 + ('DC1', 'Store_C', 5) : 2 + ('DC1', 'Store_C', 6) : 2 + ('DC1', 'Store_D', 0) : 2 + ('DC1', 'Store_D', 1) : 2 + ('DC1', 'Store_D', 2) : 2 + ('DC1', 'Store_D', 3) : 2 + ('DC1', 'Store_D', 4) : 2 + ('DC1', 'Store_D', 5) : 2 + ('DC1', 'Store_D', 6) : 2 + ('DC2', 'Store_A', 0) : 1 + ('DC2', 'Store_A', 1) : 1 + ('DC2', 'Store_A', 2) : 1 + ('DC2', 'Store_A', 3) : 1 + ('DC2', 'Store_A', 4) : 1 + ('DC2', 'Store_A', 5) : 1 + ('DC2', 'Store_A', 6) : 1 + ('DC2', 'Store_B', 0) : 1 + ('DC2', 'Store_B', 1) : 1 + ('DC2', 'Store_B', 2) : 1 + ('DC2', 'Store_B', 3) : 1 + ('DC2', 'Store_B', 4) : 1 + ('DC2', 'Store_B', 5) : 1 + ('DC2', 'Store_B', 6) : 1 + ('DC2', 'Store_C', 0) : 1 + ('DC2', 'Store_C', 1) : 1 + ('DC2', 'Store_C', 2) : 1 + ('DC2', 'Store_C', 3) : 1 + ('DC2', 'Store_C', 4) : 1 + ('DC2', 'Store_C', 5) : 1 + ('DC2', 'Store_C', 6) : 1 + ('DC2', 'Store_D', 0) : 1 + ('DC2', 'Store_D', 1) : 1 + ('DC2', 'Store_D', 2) : 1 + ('DC2', 'Store_D', 3) : 1 + ('DC2', 'Store_D', 4) : 1 + ('DC2', 'Store_D', 5) : 1 + ('DC2', 'Store_D', 6) : 1 + ('DC3', 'Store_A', 0) : 2 + ('DC3', 'Store_A', 1) : 2 + ('DC3', 'Store_A', 2) : 2 + ('DC3', 'Store_A', 3) : 2 + ('DC3', 'Store_A', 4) : 2 + ('DC3', 'Store_A', 5) : 2 + ('DC3', 'Store_A', 6) : 2 + ('DC3', 'Store_B', 0) : 2 + ('DC3', 'Store_B', 1) : 2 + ('DC3', 'Store_B', 2) : 2 + ('DC3', 'Store_B', 3) : 2 + ('DC3', 'Store_B', 4) : 2 + ('DC3', 'Store_B', 5) : 2 + ('DC3', 'Store_B', 6) : 2 + ('DC3', 'Store_C', 0) : 1 + ('DC3', 'Store_C', 1) : 1 + ('DC3', 'Store_C', 2) : 1 + ('DC3', 'Store_C', 3) : 1 + ('DC3', 'Store_C', 4) : 1 + ('DC3', 'Store_C', 5) : 1 + ('DC3', 'Store_C', 6) : 1 + ('DC3', 'Store_D', 0) : 1 + ('DC3', 'Store_D', 1) : 1 + ('DC3', 'Store_D', 2) : 1 + ('DC3', 'Store_D', 3) : 1 + ('DC3', 'Store_D', 4) : 1 + ('DC3', 'Store_D', 5) : 1 + ('DC3', 'Store_D', 6) : 1 + M : Size=1, Index=None, Domain=Any, Default=None, Mutable=True + Key : Value + None : 10000 + RP : Reoder Point + Size=28, Index=R*T, Domain=Any, Default=None, Mutable=True + Key : Value + ('Store_A', 0) : 25 + ('Store_A', 1) : 25 + ('Store_A', 2) : 25 + ('Store_A', 3) : 25 + ('Store_A', 4) : 25 + ('Store_A', 5) : 25 + ('Store_A', 6) : 25 + ('Store_B', 0) : 20 + ('Store_B', 1) : 20 + ('Store_B', 2) : 20 + ('Store_B', 3) : 20 + ('Store_B', 4) : 20 + ('Store_B', 5) : 20 + ('Store_B', 6) : 20 + ('Store_C', 0) : 15 + ('Store_C', 1) : 15 + ('Store_C', 2) : 15 + ('Store_C', 3) : 15 + ('Store_C', 4) : 15 + ('Store_C', 5) : 15 + ('Store_C', 6) : 15 + ('Store_D', 0) : 22 + ('Store_D', 1) : 22 + ('Store_D', 2) : 22 + ('Store_D', 3) : 22 + ('Store_D', 4) : 22 + ('Store_D', 5) : 22 + ('Store_D', 6) : 22 + RUTL : Reorder up to level + Size=28, Index=R*T, Domain=Any, Default=None, Mutable=True + Key : Value + ('Store_A', 0) : 80 + ('Store_A', 1) : 80 + ('Store_A', 2) : 80 + ('Store_A', 3) : 80 + ('Store_A', 4) : 80 + ('Store_A', 5) : 80 + ('Store_A', 6) : 80 + ('Store_B', 0) : 60 + ('Store_B', 1) : 60 + ('Store_B', 2) : 60 + ('Store_B', 3) : 60 + ('Store_B', 4) : 60 + ('Store_B', 5) : 60 + ('Store_B', 6) : 60 + ('Store_C', 0) : 45 + ('Store_C', 1) : 45 + ('Store_C', 2) : 45 + ('Store_C', 3) : 45 + ('Store_C', 4) : 45 + ('Store_C', 5) : 45 + ('Store_C', 6) : 45 + ('Store_D', 0) : 60 + ('Store_D', 1) : 60 + ('Store_D', 2) : 60 + ('Store_D', 3) : 60 + ('Store_D', 4) : 60 + ('Store_D', 5) : 60 + ('Store_D', 6) : 60 + aim : Aimed quantity at receiver j at time t + Size=28, Index=R*T, Domain=Any, Default=None, Mutable=True + Key : Value + ('Store_A', 0) : 40 + ('Store_A', 1) : 34 + ('Store_A', 2) : 38 + ('Store_A', 3) : 32 + ('Store_A', 4) : 36 + ('Store_A', 5) : 43 + ('Store_A', 6) : 40 + ('Store_B', 0) : 32 + ('Store_B', 1) : 30 + ('Store_B', 2) : 27 + ('Store_B', 3) : 34 + ('Store_B', 4) : 32 + ('Store_B', 5) : 36 + ('Store_B', 6) : 31 + ('Store_C', 0) : 20 + ('Store_C', 1) : 23 + ('Store_C', 2) : 21 + ('Store_C', 3) : 24 + ('Store_C', 4) : 26 + ('Store_C', 5) : 29 + ('Store_C', 6) : 30 + ('Store_D', 0) : 26 + ('Store_D', 1) : 28 + ('Store_D', 2) : 30 + ('Store_D', 3) : 29 + ('Store_D', 4) : 32 + ('Store_D', 5) : 34 + ('Store_D', 6) : 30 + c : Cost of item to go from i to j + Size=12, Index=S*R, Domain=Any, Default=None, Mutable=True + Key : Value + ('DC1', 'Store_A') : 5 + ('DC1', 'Store_B') : 7 + ('DC1', 'Store_C') : 15 + ('DC1', 'Store_D') : 18 + ('DC2', 'Store_A') : 12 + ('DC2', 'Store_B') : 10 + ('DC2', 'Store_C') : 8 + ('DC2', 'Store_D') : 11 + ('DC3', 'Store_A') : 20 + ('DC3', 'Store_B') : 16 + ('DC3', 'Store_C') : 6 + ('DC3', 'Store_D') : 5 + d : Demand at receiver j at time t + Size=28, Index=R*T, Domain=Any, Default=None, Mutable=True + Key : Value + ('Store_A', 0) : 18 + ('Store_A', 1) : 20 + ('Store_A', 2) : 19 + ('Store_A', 3) : 21 + ('Store_A', 4) : 22 + ('Store_A', 5) : 28 + ('Store_A', 6) : 26 + ('Store_B', 0) : 15 + ('Store_B', 1) : 16 + ('Store_B', 2) : 14 + ('Store_B', 3) : 17 + ('Store_B', 4) : 18 + ('Store_B', 5) : 22 + ('Store_B', 6) : 20 + ('Store_C', 0) : 10 + ('Store_C', 1) : 11 + ('Store_C', 2) : 12 + ('Store_C', 3) : 13 + ('Store_C', 4) : 14 + ('Store_C', 5) : 16 + ('Store_C', 6) : 15 + ('Store_D', 0) : 16 + ('Store_D', 1) : 17 + ('Store_D', 2) : 18 + ('Store_D', 3) : 17 + ('Store_D', 4) : 19 + ('Store_D', 5) : 24 + ('Store_D', 6) : 21 + need : Need at location j at time t + Size=28, Index=R*T, Domain=Any, Default=None, Mutable=True + Key : Value + ('Store_A', 0) : 45 + ('Store_A', 1) : 38 + ('Store_A', 2) : 42 + ('Store_A', 3) : 35 + ('Store_A', 4) : 40 + ('Store_A', 5) : 48 + ('Store_A', 6) : 45 + ('Store_B', 0) : 38 + ('Store_B', 1) : 35 + ('Store_B', 2) : 32 + ('Store_B', 3) : 40 + ('Store_B', 4) : 38 + ('Store_B', 5) : 42 + ('Store_B', 6) : 36 + ('Store_C', 0) : 27 + ('Store_C', 1) : 30 + ('Store_C', 2) : 28 + ('Store_C', 3) : 32 + ('Store_C', 4) : 35 + ('Store_C', 5) : 38 + ('Store_C', 6) : 40 + ('Store_D', 0) : 32 + ('Store_D', 1) : 35 + ('Store_D', 2) : 38 + ('Store_D', 3) : 36 + ('Store_D', 4) : 40 + ('Store_D', 5) : 42 + ('Store_D', 6) : 38 + need_min_param : limit for need quantites + Size=1, Index=None, Domain=Any, Default=None, Mutable=True + Key : Value + None : 10 + s0_i : Initial inventory of senders + Size=3, Index=S, Domain=Any, Default=None, Mutable=True + Key : Value + DC1 : 450 + DC2 : 380 + DC3 : 420 + s0_j : Initial inventory of receipients + Size=4, Index=R, Domain=Any, Default=None, Mutable=True + Key : Value + Store_A : 35 + Store_B : 22 + Store_C : 18 + Store_D : 28 + +7 Var Declarations + NI : Net inventory at receiver j at time t + Size=28, Index=R*T + Key : Lower : Value : Upper : Fixed : Stale : Domain + ('Store_A', 0) : 0 : None : None : False : True : NonNegativeReals + ('Store_A', 1) : 0 : None : None : False : True : NonNegativeReals + ('Store_A', 2) : 0 : None : None : False : True : NonNegativeReals + ('Store_A', 3) : 0 : None : None : False : True : NonNegativeReals + ('Store_A', 4) : 0 : None : None : False : True : NonNegativeReals + ('Store_A', 5) : 0 : None : None : False : True : NonNegativeReals + ('Store_A', 6) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 0) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 1) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 2) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 3) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 4) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 5) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 6) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 0) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 1) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 2) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 3) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 4) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 5) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 6) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 0) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 1) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 2) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 3) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 4) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 5) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 6) : 0 : None : None : False : True : NonNegativeReals + U_minus : Size=1, Index=None + Key : Lower : Value : Upper : Fixed : Stale : Domain + None : 0 : None : None : False : True : NonNegativeReals + U_plus : Size=1, Index=None + Key : Lower : Value : Upper : Fixed : Stale : Domain + None : 0 : None : None : False : True : NonNegativeReals + q : Quantity ordered from location i to j at time t + Size=84, Index=S*R*T + Key : Lower : Value : Upper : Fixed : Stale : Domain + ('DC1', 'Store_A', 0) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_A', 1) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_A', 2) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_A', 3) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_A', 4) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_A', 5) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_A', 6) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_B', 0) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_B', 1) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_B', 2) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_B', 3) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_B', 4) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_B', 5) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_B', 6) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_C', 0) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_C', 1) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_C', 2) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_C', 3) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_C', 4) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_C', 5) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_C', 6) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_D', 0) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_D', 1) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_D', 2) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_D', 3) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_D', 4) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_D', 5) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 'Store_D', 6) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_A', 0) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_A', 1) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_A', 2) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_A', 3) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_A', 4) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_A', 5) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_A', 6) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_B', 0) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_B', 1) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_B', 2) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_B', 3) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_B', 4) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_B', 5) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_B', 6) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_C', 0) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_C', 1) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_C', 2) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_C', 3) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_C', 4) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_C', 5) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_C', 6) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_D', 0) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_D', 1) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_D', 2) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_D', 3) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_D', 4) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_D', 5) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 'Store_D', 6) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_A', 0) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_A', 1) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_A', 2) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_A', 3) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_A', 4) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_A', 5) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_A', 6) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_B', 0) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_B', 1) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_B', 2) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_B', 3) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_B', 4) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_B', 5) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_B', 6) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_C', 0) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_C', 1) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_C', 2) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_C', 3) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_C', 4) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_C', 5) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_C', 6) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_D', 0) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_D', 1) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_D', 2) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_D', 3) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_D', 4) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_D', 5) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 'Store_D', 6) : 0 : None : None : False : True : NonNegativeReals + s : On-hand inventory at location i at time t + Size=49, Index={DC1, DC2, DC3, Store_A, Store_B, Store_C, Store_D}*T + Key : Lower : Value : Upper : Fixed : Stale : Domain + ('DC1', 0) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 1) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 2) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 3) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 4) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 5) : 0 : None : None : False : True : NonNegativeReals + ('DC1', 6) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 0) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 1) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 2) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 3) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 4) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 5) : 0 : None : None : False : True : NonNegativeReals + ('DC2', 6) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 0) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 1) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 2) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 3) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 4) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 5) : 0 : None : None : False : True : NonNegativeReals + ('DC3', 6) : 0 : None : None : False : True : NonNegativeReals + ('Store_A', 0) : 0 : None : None : False : True : NonNegativeReals + ('Store_A', 1) : 0 : None : None : False : True : NonNegativeReals + ('Store_A', 2) : 0 : None : None : False : True : NonNegativeReals + ('Store_A', 3) : 0 : None : None : False : True : NonNegativeReals + ('Store_A', 4) : 0 : None : None : False : True : NonNegativeReals + ('Store_A', 5) : 0 : None : None : False : True : NonNegativeReals + ('Store_A', 6) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 0) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 1) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 2) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 3) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 4) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 5) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 6) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 0) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 1) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 2) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 3) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 4) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 5) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 6) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 0) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 1) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 2) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 3) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 4) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 5) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 6) : 0 : None : None : False : True : NonNegativeReals + sls : Expected Sales at j at time t + Size=28, Index=R*T + Key : Lower : Value : Upper : Fixed : Stale : Domain + ('Store_A', 0) : 0 : None : None : False : True : NonNegativeReals + ('Store_A', 1) : 0 : None : None : False : True : NonNegativeReals + ('Store_A', 2) : 0 : None : None : False : True : NonNegativeReals + ('Store_A', 3) : 0 : None : None : False : True : NonNegativeReals + ('Store_A', 4) : 0 : None : None : False : True : NonNegativeReals + ('Store_A', 5) : 0 : None : None : False : True : NonNegativeReals + ('Store_A', 6) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 0) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 1) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 2) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 3) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 4) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 5) : 0 : None : None : False : True : NonNegativeReals + ('Store_B', 6) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 0) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 1) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 2) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 3) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 4) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 5) : 0 : None : None : False : True : NonNegativeReals + ('Store_C', 6) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 0) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 1) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 2) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 3) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 4) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 5) : 0 : None : None : False : True : NonNegativeReals + ('Store_D', 6) : 0 : None : None : False : True : NonNegativeReals + x : Binary: 1 if shipping from i to j at t + Size=84, Index=S*R*T + Key : Lower : Value : Upper : Fixed : Stale : Domain + ('DC1', 'Store_A', 0) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_A', 1) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_A', 2) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_A', 3) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_A', 4) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_A', 5) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_A', 6) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_B', 0) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_B', 1) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_B', 2) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_B', 3) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_B', 4) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_B', 5) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_B', 6) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_C', 0) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_C', 1) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_C', 2) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_C', 3) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_C', 4) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_C', 5) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_C', 6) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_D', 0) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_D', 1) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_D', 2) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_D', 3) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_D', 4) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_D', 5) : 0 : None : 1 : False : True : Binary + ('DC1', 'Store_D', 6) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_A', 0) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_A', 1) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_A', 2) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_A', 3) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_A', 4) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_A', 5) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_A', 6) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_B', 0) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_B', 1) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_B', 2) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_B', 3) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_B', 4) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_B', 5) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_B', 6) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_C', 0) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_C', 1) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_C', 2) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_C', 3) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_C', 4) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_C', 5) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_C', 6) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_D', 0) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_D', 1) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_D', 2) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_D', 3) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_D', 4) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_D', 5) : 0 : None : 1 : False : True : Binary + ('DC2', 'Store_D', 6) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_A', 0) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_A', 1) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_A', 2) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_A', 3) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_A', 4) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_A', 5) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_A', 6) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_B', 0) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_B', 1) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_B', 2) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_B', 3) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_B', 4) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_B', 5) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_B', 6) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_C', 0) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_C', 1) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_C', 2) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_C', 3) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_C', 4) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_C', 5) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_C', 6) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_D', 0) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_D', 1) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_D', 2) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_D', 3) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_D', 4) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_D', 5) : 0 : None : 1 : False : True : Binary + ('DC3', 'Store_D', 6) : 0 : None : 1 : False : True : Binary + +1 Objective Declarations + obj : Size=1, Index=None, Active=True + Key : Active : Sense : Expression + None : True : minimize : U_plus + U_minus + c[DC1,Store_A]*x[DC1,Store_A,0] + c[DC1,Store_A]*x[DC1,Store_A,1] + c[DC1,Store_A]*x[DC1,Store_A,2] + c[DC1,Store_A]*x[DC1,Store_A,3] + c[DC1,Store_A]*x[DC1,Store_A,4] + c[DC1,Store_A]*x[DC1,Store_A,5] + c[DC1,Store_A]*x[DC1,Store_A,6] + c[DC1,Store_B]*x[DC1,Store_B,0] + c[DC1,Store_B]*x[DC1,Store_B,1] + c[DC1,Store_B]*x[DC1,Store_B,2] + c[DC1,Store_B]*x[DC1,Store_B,3] + c[DC1,Store_B]*x[DC1,Store_B,4] + c[DC1,Store_B]*x[DC1,Store_B,5] + c[DC1,Store_B]*x[DC1,Store_B,6] + c[DC1,Store_C]*x[DC1,Store_C,0] + c[DC1,Store_C]*x[DC1,Store_C,1] + c[DC1,Store_C]*x[DC1,Store_C,2] + c[DC1,Store_C]*x[DC1,Store_C,3] + c[DC1,Store_C]*x[DC1,Store_C,4] + c[DC1,Store_C]*x[DC1,Store_C,5] + c[DC1,Store_C]*x[DC1,Store_C,6] + c[DC1,Store_D]*x[DC1,Store_D,0] + c[DC1,Store_D]*x[DC1,Store_D,1] + c[DC1,Store_D]*x[DC1,Store_D,2] + c[DC1,Store_D]*x[DC1,Store_D,3] + c[DC1,Store_D]*x[DC1,Store_D,4] + c[DC1,Store_D]*x[DC1,Store_D,5] + c[DC1,Store_D]*x[DC1,Store_D,6] + c[DC2,Store_A]*x[DC2,Store_A,0] + c[DC2,Store_A]*x[DC2,Store_A,1] + c[DC2,Store_A]*x[DC2,Store_A,2] + c[DC2,Store_A]*x[DC2,Store_A,3] + c[DC2,Store_A]*x[DC2,Store_A,4] + c[DC2,Store_A]*x[DC2,Store_A,5] + c[DC2,Store_A]*x[DC2,Store_A,6] + c[DC2,Store_B]*x[DC2,Store_B,0] + c[DC2,Store_B]*x[DC2,Store_B,1] + c[DC2,Store_B]*x[DC2,Store_B,2] + c[DC2,Store_B]*x[DC2,Store_B,3] + c[DC2,Store_B]*x[DC2,Store_B,4] + c[DC2,Store_B]*x[DC2,Store_B,5] + c[DC2,Store_B]*x[DC2,Store_B,6] + c[DC2,Store_C]*x[DC2,Store_C,0] + c[DC2,Store_C]*x[DC2,Store_C,1] + c[DC2,Store_C]*x[DC2,Store_C,2] + c[DC2,Store_C]*x[DC2,Store_C,3] + c[DC2,Store_C]*x[DC2,Store_C,4] + c[DC2,Store_C]*x[DC2,Store_C,5] + c[DC2,Store_C]*x[DC2,Store_C,6] + c[DC2,Store_D]*x[DC2,Store_D,0] + c[DC2,Store_D]*x[DC2,Store_D,1] + c[DC2,Store_D]*x[DC2,Store_D,2] + c[DC2,Store_D]*x[DC2,Store_D,3] + c[DC2,Store_D]*x[DC2,Store_D,4] + c[DC2,Store_D]*x[DC2,Store_D,5] + c[DC2,Store_D]*x[DC2,Store_D,6] + c[DC3,Store_A]*x[DC3,Store_A,0] + c[DC3,Store_A]*x[DC3,Store_A,1] + c[DC3,Store_A]*x[DC3,Store_A,2] + c[DC3,Store_A]*x[DC3,Store_A,3] + c[DC3,Store_A]*x[DC3,Store_A,4] + c[DC3,Store_A]*x[DC3,Store_A,5] + c[DC3,Store_A]*x[DC3,Store_A,6] + c[DC3,Store_B]*x[DC3,Store_B,0] + c[DC3,Store_B]*x[DC3,Store_B,1] + c[DC3,Store_B]*x[DC3,Store_B,2] + c[DC3,Store_B]*x[DC3,Store_B,3] + c[DC3,Store_B]*x[DC3,Store_B,4] + c[DC3,Store_B]*x[DC3,Store_B,5] + c[DC3,Store_B]*x[DC3,Store_B,6] + c[DC3,Store_C]*x[DC3,Store_C,0] + c[DC3,Store_C]*x[DC3,Store_C,1] + c[DC3,Store_C]*x[DC3,Store_C,2] + c[DC3,Store_C]*x[DC3,Store_C,3] + c[DC3,Store_C]*x[DC3,Store_C,4] + c[DC3,Store_C]*x[DC3,Store_C,5] + c[DC3,Store_C]*x[DC3,Store_C,6] + c[DC3,Store_D]*x[DC3,Store_D,0] + c[DC3,Store_D]*x[DC3,Store_D,1] + c[DC3,Store_D]*x[DC3,Store_D,2] + c[DC3,Store_D]*x[DC3,Store_D,3] + c[DC3,Store_D]*x[DC3,Store_D,4] + c[DC3,Store_D]*x[DC3,Store_D,5] + c[DC3,Store_D]*x[DC3,Store_D,6] - 2*(q[DC1,Store_A,0] + q[DC1,Store_A,1] + q[DC1,Store_A,2] + q[DC1,Store_A,3] + q[DC1,Store_A,4] + q[DC1,Store_A,5] + q[DC1,Store_A,6] + q[DC1,Store_B,0] + q[DC1,Store_B,1] + q[DC1,Store_B,2] + q[DC1,Store_B,3] + q[DC1,Store_B,4] + q[DC1,Store_B,5] + q[DC1,Store_B,6] + q[DC1,Store_C,0] + q[DC1,Store_C,1] + q[DC1,Store_C,2] + q[DC1,Store_C,3] + q[DC1,Store_C,4] + q[DC1,Store_C,5] + q[DC1,Store_C,6] + q[DC1,Store_D,0] + q[DC1,Store_D,1] + q[DC1,Store_D,2] + q[DC1,Store_D,3] + q[DC1,Store_D,4] + q[DC1,Store_D,5] + q[DC1,Store_D,6] + q[DC2,Store_A,0] + q[DC2,Store_A,1] + q[DC2,Store_A,2] + q[DC2,Store_A,3] + q[DC2,Store_A,4] + q[DC2,Store_A,5] + q[DC2,Store_A,6] + q[DC2,Store_B,0] + q[DC2,Store_B,1] + q[DC2,Store_B,2] + q[DC2,Store_B,3] + q[DC2,Store_B,4] + q[DC2,Store_B,5] + q[DC2,Store_B,6] + q[DC2,Store_C,0] + q[DC2,Store_C,1] + q[DC2,Store_C,2] + q[DC2,Store_C,3] + q[DC2,Store_C,4] + q[DC2,Store_C,5] + q[DC2,Store_C,6] + q[DC2,Store_D,0] + q[DC2,Store_D,1] + q[DC2,Store_D,2] + q[DC2,Store_D,3] + q[DC2,Store_D,4] + q[DC2,Store_D,5] + q[DC2,Store_D,6] + q[DC3,Store_A,0] + q[DC3,Store_A,1] + q[DC3,Store_A,2] + q[DC3,Store_A,3] + q[DC3,Store_A,4] + q[DC3,Store_A,5] + q[DC3,Store_A,6] + q[DC3,Store_B,0] + q[DC3,Store_B,1] + q[DC3,Store_B,2] + q[DC3,Store_B,3] + q[DC3,Store_B,4] + q[DC3,Store_B,5] + q[DC3,Store_B,6] + q[DC3,Store_C,0] + q[DC3,Store_C,1] + q[DC3,Store_C,2] + q[DC3,Store_C,3] + q[DC3,Store_C,4] + q[DC3,Store_C,5] + q[DC3,Store_C,6] + q[DC3,Store_D,0] + q[DC3,Store_D,1] + q[DC3,Store_D,2] + q[DC3,Store_D,3] + q[DC3,Store_D,4] + q[DC3,Store_D,5] + q[DC3,Store_D,6]) + +16 Constraint Declarations + big_m_constraint : Big M constraint: (M * sum over i of x[i,j,t]) >= NI[j,t] - RP[j,t] for all j in R, t + Size=28, Index=R*T, Active=True + Key : Lower : Body : Upper : Active + ('Store_A', 0) : -Inf : NI[Store_A,0] - RP[Store_A,0] - M*(x[DC1,Store_A,0] + x[DC2,Store_A,0] + x[DC3,Store_A,0]) : 0.0 : True + ('Store_A', 1) : -Inf : NI[Store_A,1] - RP[Store_A,1] - M*(x[DC1,Store_A,1] + x[DC2,Store_A,1] + x[DC3,Store_A,1]) : 0.0 : True + ('Store_A', 2) : -Inf : NI[Store_A,2] - RP[Store_A,2] - M*(x[DC1,Store_A,2] + x[DC2,Store_A,2] + x[DC3,Store_A,2]) : 0.0 : True + ('Store_A', 3) : -Inf : NI[Store_A,3] - RP[Store_A,3] - M*(x[DC1,Store_A,3] + x[DC2,Store_A,3] + x[DC3,Store_A,3]) : 0.0 : True + ('Store_A', 4) : -Inf : NI[Store_A,4] - RP[Store_A,4] - M*(x[DC1,Store_A,4] + x[DC2,Store_A,4] + x[DC3,Store_A,4]) : 0.0 : True + ('Store_A', 5) : -Inf : NI[Store_A,5] - RP[Store_A,5] - M*(x[DC1,Store_A,5] + x[DC2,Store_A,5] + x[DC3,Store_A,5]) : 0.0 : True + ('Store_A', 6) : -Inf : NI[Store_A,6] - RP[Store_A,6] - M*(x[DC1,Store_A,6] + x[DC2,Store_A,6] + x[DC3,Store_A,6]) : 0.0 : True + ('Store_B', 0) : -Inf : NI[Store_B,0] - RP[Store_B,0] - M*(x[DC1,Store_B,0] + x[DC2,Store_B,0] + x[DC3,Store_B,0]) : 0.0 : True + ('Store_B', 1) : -Inf : NI[Store_B,1] - RP[Store_B,1] - M*(x[DC1,Store_B,1] + x[DC2,Store_B,1] + x[DC3,Store_B,1]) : 0.0 : True + ('Store_B', 2) : -Inf : NI[Store_B,2] - RP[Store_B,2] - M*(x[DC1,Store_B,2] + x[DC2,Store_B,2] + x[DC3,Store_B,2]) : 0.0 : True + ('Store_B', 3) : -Inf : NI[Store_B,3] - RP[Store_B,3] - M*(x[DC1,Store_B,3] + x[DC2,Store_B,3] + x[DC3,Store_B,3]) : 0.0 : True + ('Store_B', 4) : -Inf : NI[Store_B,4] - RP[Store_B,4] - M*(x[DC1,Store_B,4] + x[DC2,Store_B,4] + x[DC3,Store_B,4]) : 0.0 : True + ('Store_B', 5) : -Inf : NI[Store_B,5] - RP[Store_B,5] - M*(x[DC1,Store_B,5] + x[DC2,Store_B,5] + x[DC3,Store_B,5]) : 0.0 : True + ('Store_B', 6) : -Inf : NI[Store_B,6] - RP[Store_B,6] - M*(x[DC1,Store_B,6] + x[DC2,Store_B,6] + x[DC3,Store_B,6]) : 0.0 : True + ('Store_C', 0) : -Inf : NI[Store_C,0] - RP[Store_C,0] - M*(x[DC1,Store_C,0] + x[DC2,Store_C,0] + x[DC3,Store_C,0]) : 0.0 : True + ('Store_C', 1) : -Inf : NI[Store_C,1] - RP[Store_C,1] - M*(x[DC1,Store_C,1] + x[DC2,Store_C,1] + x[DC3,Store_C,1]) : 0.0 : True + ('Store_C', 2) : -Inf : NI[Store_C,2] - RP[Store_C,2] - M*(x[DC1,Store_C,2] + x[DC2,Store_C,2] + x[DC3,Store_C,2]) : 0.0 : True + ('Store_C', 3) : -Inf : NI[Store_C,3] - RP[Store_C,3] - M*(x[DC1,Store_C,3] + x[DC2,Store_C,3] + x[DC3,Store_C,3]) : 0.0 : True + ('Store_C', 4) : -Inf : NI[Store_C,4] - RP[Store_C,4] - M*(x[DC1,Store_C,4] + x[DC2,Store_C,4] + x[DC3,Store_C,4]) : 0.0 : True + ('Store_C', 5) : -Inf : NI[Store_C,5] - RP[Store_C,5] - M*(x[DC1,Store_C,5] + x[DC2,Store_C,5] + x[DC3,Store_C,5]) : 0.0 : True + ('Store_C', 6) : -Inf : NI[Store_C,6] - RP[Store_C,6] - M*(x[DC1,Store_C,6] + x[DC2,Store_C,6] + x[DC3,Store_C,6]) : 0.0 : True + ('Store_D', 0) : -Inf : NI[Store_D,0] - RP[Store_D,0] - M*(x[DC1,Store_D,0] + x[DC2,Store_D,0] + x[DC3,Store_D,0]) : 0.0 : True + ('Store_D', 1) : -Inf : NI[Store_D,1] - RP[Store_D,1] - M*(x[DC1,Store_D,1] + x[DC2,Store_D,1] + x[DC3,Store_D,1]) : 0.0 : True + ('Store_D', 2) : -Inf : NI[Store_D,2] - RP[Store_D,2] - M*(x[DC1,Store_D,2] + x[DC2,Store_D,2] + x[DC3,Store_D,2]) : 0.0 : True + ('Store_D', 3) : -Inf : NI[Store_D,3] - RP[Store_D,3] - M*(x[DC1,Store_D,3] + x[DC2,Store_D,3] + x[DC3,Store_D,3]) : 0.0 : True + ('Store_D', 4) : -Inf : NI[Store_D,4] - RP[Store_D,4] - M*(x[DC1,Store_D,4] + x[DC2,Store_D,4] + x[DC3,Store_D,4]) : 0.0 : True + ('Store_D', 5) : -Inf : NI[Store_D,5] - RP[Store_D,5] - M*(x[DC1,Store_D,5] + x[DC2,Store_D,5] + x[DC3,Store_D,5]) : 0.0 : True + ('Store_D', 6) : -Inf : NI[Store_D,6] - RP[Store_D,6] - M*(x[DC1,Store_D,6] + x[DC2,Store_D,6] + x[DC3,Store_D,6]) : 0.0 : True + init_recipient_inventory : Initial recipient inventory: s[j,t0] = s0_j for all j in R + Size=4, Index=R, Active=True + Key : Lower : Body : Upper : Active + Store_A : s0_j[Store_A] : s[Store_A,0] : s0_j[Store_A] : True + Store_B : s0_j[Store_B] : s[Store_B,0] : s0_j[Store_B] : True + Store_C : s0_j[Store_C] : s[Store_C,0] : s0_j[Store_C] : True + Store_D : s0_j[Store_D] : s[Store_D,0] : s0_j[Store_D] : True + init_sender_inventory : Initial sender inventory: s[i,t0] = s0_i for all i in S + Size=3, Index=S, Active=True + Key : Lower : Body : Upper : Active + DC1 : s0_i[DC1] : s[DC1,0] : s0_i[DC1] : True + DC2 : s0_i[DC2] : s[DC2,0] : s0_i[DC2] : True + DC3 : s0_i[DC3] : s[DC3,0] : s0_i[DC3] : True + inventory_capacity : Inventory Capacity Constraint + Size=49, Index={DC1, DC2, DC3, Store_A, Store_B, Store_C, Store_D}*T, Active=True + Key : Lower : Body : Upper : Active + ('DC1', 0) : -Inf : s[DC1,0] : 150.0 : True + ('DC1', 1) : -Inf : s[DC1,1] : 150.0 : True + ('DC1', 2) : -Inf : s[DC1,2] : 150.0 : True + ('DC1', 3) : -Inf : s[DC1,3] : 150.0 : True + ('DC1', 4) : -Inf : s[DC1,4] : 150.0 : True + ('DC1', 5) : -Inf : s[DC1,5] : 150.0 : True + ('DC1', 6) : -Inf : s[DC1,6] : 150.0 : True + ('DC2', 0) : -Inf : s[DC2,0] : 350.0 : True + ('DC2', 1) : -Inf : s[DC2,1] : 350.0 : True + ('DC2', 2) : -Inf : s[DC2,2] : 350.0 : True + ('DC2', 3) : -Inf : s[DC2,3] : 350.0 : True + ('DC2', 4) : -Inf : s[DC2,4] : 350.0 : True + ('DC2', 5) : -Inf : s[DC2,5] : 350.0 : True + ('DC2', 6) : -Inf : s[DC2,6] : 350.0 : True + ('DC3', 0) : -Inf : s[DC3,0] : 380.0 : True + ('DC3', 1) : -Inf : s[DC3,1] : 380.0 : True + ('DC3', 2) : -Inf : s[DC3,2] : 380.0 : True + ('DC3', 3) : -Inf : s[DC3,3] : 380.0 : True + ('DC3', 4) : -Inf : s[DC3,4] : 380.0 : True + ('DC3', 5) : -Inf : s[DC3,5] : 380.0 : True + ('DC3', 6) : -Inf : s[DC3,6] : 380.0 : True + ('Store_A', 0) : -Inf : s[Store_A,0] : 70.0 : True + ('Store_A', 1) : -Inf : s[Store_A,1] : 70.0 : True + ('Store_A', 2) : -Inf : s[Store_A,2] : 70.0 : True + ('Store_A', 3) : -Inf : s[Store_A,3] : 70.0 : True + ('Store_A', 4) : -Inf : s[Store_A,4] : 70.0 : True + ('Store_A', 5) : -Inf : s[Store_A,5] : 70.0 : True + ('Store_A', 6) : -Inf : s[Store_A,6] : 70.0 : True + ('Store_B', 0) : -Inf : s[Store_B,0] : 50.0 : True + ('Store_B', 1) : -Inf : s[Store_B,1] : 50.0 : True + ('Store_B', 2) : -Inf : s[Store_B,2] : 50.0 : True + ('Store_B', 3) : -Inf : s[Store_B,3] : 50.0 : True + ('Store_B', 4) : -Inf : s[Store_B,4] : 50.0 : True + ('Store_B', 5) : -Inf : s[Store_B,5] : 50.0 : True + ('Store_B', 6) : -Inf : s[Store_B,6] : 50.0 : True + ('Store_C', 0) : -Inf : s[Store_C,0] : 40.0 : True + ('Store_C', 1) : -Inf : s[Store_C,1] : 40.0 : True + ('Store_C', 2) : -Inf : s[Store_C,2] : 40.0 : True + ('Store_C', 3) : -Inf : s[Store_C,3] : 40.0 : True + ('Store_C', 4) : -Inf : s[Store_C,4] : 40.0 : True + ('Store_C', 5) : -Inf : s[Store_C,5] : 40.0 : True + ('Store_C', 6) : -Inf : s[Store_C,6] : 40.0 : True + ('Store_D', 0) : -Inf : s[Store_D,0] : 55.0 : True + ('Store_D', 1) : -Inf : s[Store_D,1] : 55.0 : True + ('Store_D', 2) : -Inf : s[Store_D,2] : 55.0 : True + ('Store_D', 3) : -Inf : s[Store_D,3] : 55.0 : True + ('Store_D', 4) : -Inf : s[Store_D,4] : 55.0 : True + ('Store_D', 5) : -Inf : s[Store_D,5] : 55.0 : True + ('Store_D', 6) : -Inf : s[Store_D,6] : 55.0 : True + linking : Linking constraint: q[i,j,t] <= need[j,t] * x[i,j,t] for all i,j,t + Size=84, Index=S*R*T, Active=True + Key : Lower : Body : Upper : Active + ('DC1', 'Store_A', 0) : -Inf : q[DC1,Store_A,0] - need[Store_A,0]*x[DC1,Store_A,0] : 0.0 : True + ('DC1', 'Store_A', 1) : -Inf : q[DC1,Store_A,1] - need[Store_A,1]*x[DC1,Store_A,1] : 0.0 : True + ('DC1', 'Store_A', 2) : -Inf : q[DC1,Store_A,2] - need[Store_A,2]*x[DC1,Store_A,2] : 0.0 : True + ('DC1', 'Store_A', 3) : -Inf : q[DC1,Store_A,3] - need[Store_A,3]*x[DC1,Store_A,3] : 0.0 : True + ('DC1', 'Store_A', 4) : -Inf : q[DC1,Store_A,4] - need[Store_A,4]*x[DC1,Store_A,4] : 0.0 : True + ('DC1', 'Store_A', 5) : -Inf : q[DC1,Store_A,5] - need[Store_A,5]*x[DC1,Store_A,5] : 0.0 : True + ('DC1', 'Store_A', 6) : -Inf : q[DC1,Store_A,6] - need[Store_A,6]*x[DC1,Store_A,6] : 0.0 : True + ('DC1', 'Store_B', 0) : -Inf : q[DC1,Store_B,0] - need[Store_B,0]*x[DC1,Store_B,0] : 0.0 : True + ('DC1', 'Store_B', 1) : -Inf : q[DC1,Store_B,1] - need[Store_B,1]*x[DC1,Store_B,1] : 0.0 : True + ('DC1', 'Store_B', 2) : -Inf : q[DC1,Store_B,2] - need[Store_B,2]*x[DC1,Store_B,2] : 0.0 : True + ('DC1', 'Store_B', 3) : -Inf : q[DC1,Store_B,3] - need[Store_B,3]*x[DC1,Store_B,3] : 0.0 : True + ('DC1', 'Store_B', 4) : -Inf : q[DC1,Store_B,4] - need[Store_B,4]*x[DC1,Store_B,4] : 0.0 : True + ('DC1', 'Store_B', 5) : -Inf : q[DC1,Store_B,5] - need[Store_B,5]*x[DC1,Store_B,5] : 0.0 : True + ('DC1', 'Store_B', 6) : -Inf : q[DC1,Store_B,6] - need[Store_B,6]*x[DC1,Store_B,6] : 0.0 : True + ('DC1', 'Store_C', 0) : -Inf : q[DC1,Store_C,0] - need[Store_C,0]*x[DC1,Store_C,0] : 0.0 : True + ('DC1', 'Store_C', 1) : -Inf : q[DC1,Store_C,1] - need[Store_C,1]*x[DC1,Store_C,1] : 0.0 : True + ('DC1', 'Store_C', 2) : -Inf : q[DC1,Store_C,2] - need[Store_C,2]*x[DC1,Store_C,2] : 0.0 : True + ('DC1', 'Store_C', 3) : -Inf : q[DC1,Store_C,3] - need[Store_C,3]*x[DC1,Store_C,3] : 0.0 : True + ('DC1', 'Store_C', 4) : -Inf : q[DC1,Store_C,4] - need[Store_C,4]*x[DC1,Store_C,4] : 0.0 : True + ('DC1', 'Store_C', 5) : -Inf : q[DC1,Store_C,5] - need[Store_C,5]*x[DC1,Store_C,5] : 0.0 : True + ('DC1', 'Store_C', 6) : -Inf : q[DC1,Store_C,6] - need[Store_C,6]*x[DC1,Store_C,6] : 0.0 : True + ('DC1', 'Store_D', 0) : -Inf : q[DC1,Store_D,0] - need[Store_D,0]*x[DC1,Store_D,0] : 0.0 : True + ('DC1', 'Store_D', 1) : -Inf : q[DC1,Store_D,1] - need[Store_D,1]*x[DC1,Store_D,1] : 0.0 : True + ('DC1', 'Store_D', 2) : -Inf : q[DC1,Store_D,2] - need[Store_D,2]*x[DC1,Store_D,2] : 0.0 : True + ('DC1', 'Store_D', 3) : -Inf : q[DC1,Store_D,3] - need[Store_D,3]*x[DC1,Store_D,3] : 0.0 : True + ('DC1', 'Store_D', 4) : -Inf : q[DC1,Store_D,4] - need[Store_D,4]*x[DC1,Store_D,4] : 0.0 : True + ('DC1', 'Store_D', 5) : -Inf : q[DC1,Store_D,5] - need[Store_D,5]*x[DC1,Store_D,5] : 0.0 : True + ('DC1', 'Store_D', 6) : -Inf : q[DC1,Store_D,6] - need[Store_D,6]*x[DC1,Store_D,6] : 0.0 : True + ('DC2', 'Store_A', 0) : -Inf : q[DC2,Store_A,0] - need[Store_A,0]*x[DC2,Store_A,0] : 0.0 : True + ('DC2', 'Store_A', 1) : -Inf : q[DC2,Store_A,1] - need[Store_A,1]*x[DC2,Store_A,1] : 0.0 : True + ('DC2', 'Store_A', 2) : -Inf : q[DC2,Store_A,2] - need[Store_A,2]*x[DC2,Store_A,2] : 0.0 : True + ('DC2', 'Store_A', 3) : -Inf : q[DC2,Store_A,3] - need[Store_A,3]*x[DC2,Store_A,3] : 0.0 : True + ('DC2', 'Store_A', 4) : -Inf : q[DC2,Store_A,4] - need[Store_A,4]*x[DC2,Store_A,4] : 0.0 : True + ('DC2', 'Store_A', 5) : -Inf : q[DC2,Store_A,5] - need[Store_A,5]*x[DC2,Store_A,5] : 0.0 : True + ('DC2', 'Store_A', 6) : -Inf : q[DC2,Store_A,6] - need[Store_A,6]*x[DC2,Store_A,6] : 0.0 : True + ('DC2', 'Store_B', 0) : -Inf : q[DC2,Store_B,0] - need[Store_B,0]*x[DC2,Store_B,0] : 0.0 : True + ('DC2', 'Store_B', 1) : -Inf : q[DC2,Store_B,1] - need[Store_B,1]*x[DC2,Store_B,1] : 0.0 : True + ('DC2', 'Store_B', 2) : -Inf : q[DC2,Store_B,2] - need[Store_B,2]*x[DC2,Store_B,2] : 0.0 : True + ('DC2', 'Store_B', 3) : -Inf : q[DC2,Store_B,3] - need[Store_B,3]*x[DC2,Store_B,3] : 0.0 : True + ('DC2', 'Store_B', 4) : -Inf : q[DC2,Store_B,4] - need[Store_B,4]*x[DC2,Store_B,4] : 0.0 : True + ('DC2', 'Store_B', 5) : -Inf : q[DC2,Store_B,5] - need[Store_B,5]*x[DC2,Store_B,5] : 0.0 : True + ('DC2', 'Store_B', 6) : -Inf : q[DC2,Store_B,6] - need[Store_B,6]*x[DC2,Store_B,6] : 0.0 : True + ('DC2', 'Store_C', 0) : -Inf : q[DC2,Store_C,0] - need[Store_C,0]*x[DC2,Store_C,0] : 0.0 : True + ('DC2', 'Store_C', 1) : -Inf : q[DC2,Store_C,1] - need[Store_C,1]*x[DC2,Store_C,1] : 0.0 : True + ('DC2', 'Store_C', 2) : -Inf : q[DC2,Store_C,2] - need[Store_C,2]*x[DC2,Store_C,2] : 0.0 : True + ('DC2', 'Store_C', 3) : -Inf : q[DC2,Store_C,3] - need[Store_C,3]*x[DC2,Store_C,3] : 0.0 : True + ('DC2', 'Store_C', 4) : -Inf : q[DC2,Store_C,4] - need[Store_C,4]*x[DC2,Store_C,4] : 0.0 : True + ('DC2', 'Store_C', 5) : -Inf : q[DC2,Store_C,5] - need[Store_C,5]*x[DC2,Store_C,5] : 0.0 : True + ('DC2', 'Store_C', 6) : -Inf : q[DC2,Store_C,6] - need[Store_C,6]*x[DC2,Store_C,6] : 0.0 : True + ('DC2', 'Store_D', 0) : -Inf : q[DC2,Store_D,0] - need[Store_D,0]*x[DC2,Store_D,0] : 0.0 : True + ('DC2', 'Store_D', 1) : -Inf : q[DC2,Store_D,1] - need[Store_D,1]*x[DC2,Store_D,1] : 0.0 : True + ('DC2', 'Store_D', 2) : -Inf : q[DC2,Store_D,2] - need[Store_D,2]*x[DC2,Store_D,2] : 0.0 : True + ('DC2', 'Store_D', 3) : -Inf : q[DC2,Store_D,3] - need[Store_D,3]*x[DC2,Store_D,3] : 0.0 : True + ('DC2', 'Store_D', 4) : -Inf : q[DC2,Store_D,4] - need[Store_D,4]*x[DC2,Store_D,4] : 0.0 : True + ('DC2', 'Store_D', 5) : -Inf : q[DC2,Store_D,5] - need[Store_D,5]*x[DC2,Store_D,5] : 0.0 : True + ('DC2', 'Store_D', 6) : -Inf : q[DC2,Store_D,6] - need[Store_D,6]*x[DC2,Store_D,6] : 0.0 : True + ('DC3', 'Store_A', 0) : -Inf : q[DC3,Store_A,0] - need[Store_A,0]*x[DC3,Store_A,0] : 0.0 : True + ('DC3', 'Store_A', 1) : -Inf : q[DC3,Store_A,1] - need[Store_A,1]*x[DC3,Store_A,1] : 0.0 : True + ('DC3', 'Store_A', 2) : -Inf : q[DC3,Store_A,2] - need[Store_A,2]*x[DC3,Store_A,2] : 0.0 : True + ('DC3', 'Store_A', 3) : -Inf : q[DC3,Store_A,3] - need[Store_A,3]*x[DC3,Store_A,3] : 0.0 : True + ('DC3', 'Store_A', 4) : -Inf : q[DC3,Store_A,4] - need[Store_A,4]*x[DC3,Store_A,4] : 0.0 : True + ('DC3', 'Store_A', 5) : -Inf : q[DC3,Store_A,5] - need[Store_A,5]*x[DC3,Store_A,5] : 0.0 : True + ('DC3', 'Store_A', 6) : -Inf : q[DC3,Store_A,6] - need[Store_A,6]*x[DC3,Store_A,6] : 0.0 : True + ('DC3', 'Store_B', 0) : -Inf : q[DC3,Store_B,0] - need[Store_B,0]*x[DC3,Store_B,0] : 0.0 : True + ('DC3', 'Store_B', 1) : -Inf : q[DC3,Store_B,1] - need[Store_B,1]*x[DC3,Store_B,1] : 0.0 : True + ('DC3', 'Store_B', 2) : -Inf : q[DC3,Store_B,2] - need[Store_B,2]*x[DC3,Store_B,2] : 0.0 : True + ('DC3', 'Store_B', 3) : -Inf : q[DC3,Store_B,3] - need[Store_B,3]*x[DC3,Store_B,3] : 0.0 : True + ('DC3', 'Store_B', 4) : -Inf : q[DC3,Store_B,4] - need[Store_B,4]*x[DC3,Store_B,4] : 0.0 : True + ('DC3', 'Store_B', 5) : -Inf : q[DC3,Store_B,5] - need[Store_B,5]*x[DC3,Store_B,5] : 0.0 : True + ('DC3', 'Store_B', 6) : -Inf : q[DC3,Store_B,6] - need[Store_B,6]*x[DC3,Store_B,6] : 0.0 : True + ('DC3', 'Store_C', 0) : -Inf : q[DC3,Store_C,0] - need[Store_C,0]*x[DC3,Store_C,0] : 0.0 : True + ('DC3', 'Store_C', 1) : -Inf : q[DC3,Store_C,1] - need[Store_C,1]*x[DC3,Store_C,1] : 0.0 : True + ('DC3', 'Store_C', 2) : -Inf : q[DC3,Store_C,2] - need[Store_C,2]*x[DC3,Store_C,2] : 0.0 : True + ('DC3', 'Store_C', 3) : -Inf : q[DC3,Store_C,3] - need[Store_C,3]*x[DC3,Store_C,3] : 0.0 : True + ('DC3', 'Store_C', 4) : -Inf : q[DC3,Store_C,4] - need[Store_C,4]*x[DC3,Store_C,4] : 0.0 : True + ('DC3', 'Store_C', 5) : -Inf : q[DC3,Store_C,5] - need[Store_C,5]*x[DC3,Store_C,5] : 0.0 : True + ('DC3', 'Store_C', 6) : -Inf : q[DC3,Store_C,6] - need[Store_C,6]*x[DC3,Store_C,6] : 0.0 : True + ('DC3', 'Store_D', 0) : -Inf : q[DC3,Store_D,0] - need[Store_D,0]*x[DC3,Store_D,0] : 0.0 : True + ('DC3', 'Store_D', 1) : -Inf : q[DC3,Store_D,1] - need[Store_D,1]*x[DC3,Store_D,1] : 0.0 : True + ('DC3', 'Store_D', 2) : -Inf : q[DC3,Store_D,2] - need[Store_D,2]*x[DC3,Store_D,2] : 0.0 : True + ('DC3', 'Store_D', 3) : -Inf : q[DC3,Store_D,3] - need[Store_D,3]*x[DC3,Store_D,3] : 0.0 : True + ('DC3', 'Store_D', 4) : -Inf : q[DC3,Store_D,4] - need[Store_D,4]*x[DC3,Store_D,4] : 0.0 : True + ('DC3', 'Store_D', 5) : -Inf : q[DC3,Store_D,5] - need[Store_D,5]*x[DC3,Store_D,5] : 0.0 : True + ('DC3', 'Store_D', 6) : -Inf : q[DC3,Store_D,6] - need[Store_D,6]*x[DC3,Store_D,6] : 0.0 : True + need_min : Need bounded my minimum need + Size=28, Index=R*T, Active=True + Key : Lower : Body : Upper : Active + ('Store_A', 0) : -Inf : need[Store_A,0] : need_min_param : True + ('Store_A', 1) : -Inf : need[Store_A,1] : need_min_param : True + ('Store_A', 2) : -Inf : need[Store_A,2] : need_min_param : True + ('Store_A', 3) : -Inf : need[Store_A,3] : need_min_param : True + ('Store_A', 4) : -Inf : need[Store_A,4] : need_min_param : True + ('Store_A', 5) : -Inf : need[Store_A,5] : need_min_param : True + ('Store_A', 6) : -Inf : need[Store_A,6] : need_min_param : True + ('Store_B', 0) : -Inf : need[Store_B,0] : need_min_param : True + ('Store_B', 1) : -Inf : need[Store_B,1] : need_min_param : True + ('Store_B', 2) : -Inf : need[Store_B,2] : need_min_param : True + ('Store_B', 3) : -Inf : need[Store_B,3] : need_min_param : True + ('Store_B', 4) : -Inf : need[Store_B,4] : need_min_param : True + ('Store_B', 5) : -Inf : need[Store_B,5] : need_min_param : True + ('Store_B', 6) : -Inf : need[Store_B,6] : need_min_param : True + ('Store_C', 0) : -Inf : need[Store_C,0] : need_min_param : True + ('Store_C', 1) : -Inf : need[Store_C,1] : need_min_param : True + ('Store_C', 2) : -Inf : need[Store_C,2] : need_min_param : True + ('Store_C', 3) : -Inf : need[Store_C,3] : need_min_param : True + ('Store_C', 4) : -Inf : need[Store_C,4] : need_min_param : True + ('Store_C', 5) : -Inf : need[Store_C,5] : need_min_param : True + ('Store_C', 6) : -Inf : need[Store_C,6] : need_min_param : True + ('Store_D', 0) : -Inf : need[Store_D,0] : need_min_param : True + ('Store_D', 1) : -Inf : need[Store_D,1] : need_min_param : True + ('Store_D', 2) : -Inf : need[Store_D,2] : need_min_param : True + ('Store_D', 3) : -Inf : need[Store_D,3] : need_min_param : True + ('Store_D', 4) : -Inf : need[Store_D,4] : need_min_param : True + ('Store_D', 5) : -Inf : need[Store_D,5] : need_min_param : True + ('Store_D', 6) : -Inf : need[Store_D,6] : need_min_param : True + net_inventory_update : NI[j,t+1] = s[j,t] + sum over i sum over u in (t-LT[i,j,t], t) of q[i,j,u] + Size=24, Index=R*T, Active=True + Key : Lower : Body : Upper : Active + ('Store_A', 0) : 0.0 : NI[Store_A,1] - (s[Store_A,0] + q[DC1,Store_A,0] + q[DC2,Store_A,0] + q[DC3,Store_A,0]) : 0.0 : True + ('Store_A', 1) : 0.0 : NI[Store_A,2] - (s[Store_A,1] + q[DC1,Store_A,1] + q[DC2,Store_A,1] + q[DC3,Store_A,0] + q[DC3,Store_A,1]) : 0.0 : True + ('Store_A', 2) : 0.0 : NI[Store_A,3] - (s[Store_A,2] + q[DC1,Store_A,2] + q[DC2,Store_A,2] + q[DC3,Store_A,1] + q[DC3,Store_A,2]) : 0.0 : True + ('Store_A', 3) : 0.0 : NI[Store_A,4] - (s[Store_A,3] + q[DC1,Store_A,3] + q[DC2,Store_A,3] + q[DC3,Store_A,2] + q[DC3,Store_A,3]) : 0.0 : True + ('Store_A', 4) : 0.0 : NI[Store_A,5] - (s[Store_A,4] + q[DC1,Store_A,4] + q[DC2,Store_A,4] + q[DC3,Store_A,3] + q[DC3,Store_A,4]) : 0.0 : True + ('Store_A', 5) : 0.0 : NI[Store_A,6] - (s[Store_A,5] + q[DC1,Store_A,5] + q[DC2,Store_A,5] + q[DC3,Store_A,4] + q[DC3,Store_A,5]) : 0.0 : True + ('Store_B', 0) : 0.0 : NI[Store_B,1] - (s[Store_B,0] + q[DC1,Store_B,0] + q[DC2,Store_B,0] + q[DC3,Store_B,0]) : 0.0 : True + ('Store_B', 1) : 0.0 : NI[Store_B,2] - (s[Store_B,1] + q[DC1,Store_B,1] + q[DC2,Store_B,1] + q[DC3,Store_B,0] + q[DC3,Store_B,1]) : 0.0 : True + ('Store_B', 2) : 0.0 : NI[Store_B,3] - (s[Store_B,2] + q[DC1,Store_B,2] + q[DC2,Store_B,2] + q[DC3,Store_B,1] + q[DC3,Store_B,2]) : 0.0 : True + ('Store_B', 3) : 0.0 : NI[Store_B,4] - (s[Store_B,3] + q[DC1,Store_B,3] + q[DC2,Store_B,3] + q[DC3,Store_B,2] + q[DC3,Store_B,3]) : 0.0 : True + ('Store_B', 4) : 0.0 : NI[Store_B,5] - (s[Store_B,4] + q[DC1,Store_B,4] + q[DC2,Store_B,4] + q[DC3,Store_B,3] + q[DC3,Store_B,4]) : 0.0 : True + ('Store_B', 5) : 0.0 : NI[Store_B,6] - (s[Store_B,5] + q[DC1,Store_B,5] + q[DC2,Store_B,5] + q[DC3,Store_B,4] + q[DC3,Store_B,5]) : 0.0 : True + ('Store_C', 0) : 0.0 : NI[Store_C,1] - (s[Store_C,0] + q[DC1,Store_C,0] + q[DC2,Store_C,0] + q[DC3,Store_C,0]) : 0.0 : True + ('Store_C', 1) : 0.0 : NI[Store_C,2] - (s[Store_C,1] + q[DC1,Store_C,0] + q[DC1,Store_C,1] + q[DC2,Store_C,1] + q[DC3,Store_C,1]) : 0.0 : True + ('Store_C', 2) : 0.0 : NI[Store_C,3] - (s[Store_C,2] + q[DC1,Store_C,1] + q[DC1,Store_C,2] + q[DC2,Store_C,2] + q[DC3,Store_C,2]) : 0.0 : True + ('Store_C', 3) : 0.0 : NI[Store_C,4] - (s[Store_C,3] + q[DC1,Store_C,2] + q[DC1,Store_C,3] + q[DC2,Store_C,3] + q[DC3,Store_C,3]) : 0.0 : True + ('Store_C', 4) : 0.0 : NI[Store_C,5] - (s[Store_C,4] + q[DC1,Store_C,3] + q[DC1,Store_C,4] + q[DC2,Store_C,4] + q[DC3,Store_C,4]) : 0.0 : True + ('Store_C', 5) : 0.0 : NI[Store_C,6] - (s[Store_C,5] + q[DC1,Store_C,4] + q[DC1,Store_C,5] + q[DC2,Store_C,5] + q[DC3,Store_C,5]) : 0.0 : True + ('Store_D', 0) : 0.0 : NI[Store_D,1] - (s[Store_D,0] + q[DC1,Store_D,0] + q[DC2,Store_D,0] + q[DC3,Store_D,0]) : 0.0 : True + ('Store_D', 1) : 0.0 : NI[Store_D,2] - (s[Store_D,1] + q[DC1,Store_D,0] + q[DC1,Store_D,1] + q[DC2,Store_D,1] + q[DC3,Store_D,1]) : 0.0 : True + ('Store_D', 2) : 0.0 : NI[Store_D,3] - (s[Store_D,2] + q[DC1,Store_D,1] + q[DC1,Store_D,2] + q[DC2,Store_D,2] + q[DC3,Store_D,2]) : 0.0 : True + ('Store_D', 3) : 0.0 : NI[Store_D,4] - (s[Store_D,3] + q[DC1,Store_D,2] + q[DC1,Store_D,3] + q[DC2,Store_D,3] + q[DC3,Store_D,3]) : 0.0 : True + ('Store_D', 4) : 0.0 : NI[Store_D,5] - (s[Store_D,4] + q[DC1,Store_D,3] + q[DC1,Store_D,4] + q[DC2,Store_D,4] + q[DC3,Store_D,4]) : 0.0 : True + ('Store_D', 5) : 0.0 : NI[Store_D,6] - (s[Store_D,5] + q[DC1,Store_D,4] + q[DC1,Store_D,5] + q[DC2,Store_D,5] + q[DC3,Store_D,5]) : 0.0 : True + recipient_inventory_update : s[j,t+1] = s[j,t] - sls[j,t] + sum over i of q[i,j,t-LT[i,j,t]] + Size=24, Index=R*T, Active=True + Key : Lower : Body : Upper : Active + ('Store_A', 0) : 0.0 : s[Store_A,1] - (s[Store_A,0] - sls[Store_A,0]) : 0.0 : True + ('Store_A', 1) : 0.0 : s[Store_A,2] - (s[Store_A,1] - sls[Store_A,1] + q[DC1,Store_A,0] + q[DC2,Store_A,0]) : 0.0 : True + ('Store_A', 2) : 0.0 : s[Store_A,3] - (s[Store_A,2] - sls[Store_A,2] + q[DC1,Store_A,1] + q[DC2,Store_A,1] + q[DC3,Store_A,0]) : 0.0 : True + ('Store_A', 3) : 0.0 : s[Store_A,4] - (s[Store_A,3] - sls[Store_A,3] + q[DC1,Store_A,2] + q[DC2,Store_A,2] + q[DC3,Store_A,1]) : 0.0 : True + ('Store_A', 4) : 0.0 : s[Store_A,5] - (s[Store_A,4] - sls[Store_A,4] + q[DC1,Store_A,3] + q[DC2,Store_A,3] + q[DC3,Store_A,2]) : 0.0 : True + ('Store_A', 5) : 0.0 : s[Store_A,6] - (s[Store_A,5] - sls[Store_A,5] + q[DC1,Store_A,4] + q[DC2,Store_A,4] + q[DC3,Store_A,3]) : 0.0 : True + ('Store_B', 0) : 0.0 : s[Store_B,1] - (s[Store_B,0] - sls[Store_B,0]) : 0.0 : True + ('Store_B', 1) : 0.0 : s[Store_B,2] - (s[Store_B,1] - sls[Store_B,1] + q[DC1,Store_B,0] + q[DC2,Store_B,0]) : 0.0 : True + ('Store_B', 2) : 0.0 : s[Store_B,3] - (s[Store_B,2] - sls[Store_B,2] + q[DC1,Store_B,1] + q[DC2,Store_B,1] + q[DC3,Store_B,0]) : 0.0 : True + ('Store_B', 3) : 0.0 : s[Store_B,4] - (s[Store_B,3] - sls[Store_B,3] + q[DC1,Store_B,2] + q[DC2,Store_B,2] + q[DC3,Store_B,1]) : 0.0 : True + ('Store_B', 4) : 0.0 : s[Store_B,5] - (s[Store_B,4] - sls[Store_B,4] + q[DC1,Store_B,3] + q[DC2,Store_B,3] + q[DC3,Store_B,2]) : 0.0 : True + ('Store_B', 5) : 0.0 : s[Store_B,6] - (s[Store_B,5] - sls[Store_B,5] + q[DC1,Store_B,4] + q[DC2,Store_B,4] + q[DC3,Store_B,3]) : 0.0 : True + ('Store_C', 0) : 0.0 : s[Store_C,1] - (s[Store_C,0] - sls[Store_C,0]) : 0.0 : True + ('Store_C', 1) : 0.0 : s[Store_C,2] - (s[Store_C,1] - sls[Store_C,1] + q[DC2,Store_C,0] + q[DC3,Store_C,0]) : 0.0 : True + ('Store_C', 2) : 0.0 : s[Store_C,3] - (s[Store_C,2] - sls[Store_C,2] + q[DC1,Store_C,0] + q[DC2,Store_C,1] + q[DC3,Store_C,1]) : 0.0 : True + ('Store_C', 3) : 0.0 : s[Store_C,4] - (s[Store_C,3] - sls[Store_C,3] + q[DC1,Store_C,1] + q[DC2,Store_C,2] + q[DC3,Store_C,2]) : 0.0 : True + ('Store_C', 4) : 0.0 : s[Store_C,5] - (s[Store_C,4] - sls[Store_C,4] + q[DC1,Store_C,2] + q[DC2,Store_C,3] + q[DC3,Store_C,3]) : 0.0 : True + ('Store_C', 5) : 0.0 : s[Store_C,6] - (s[Store_C,5] - sls[Store_C,5] + q[DC1,Store_C,3] + q[DC2,Store_C,4] + q[DC3,Store_C,4]) : 0.0 : True + ('Store_D', 0) : 0.0 : s[Store_D,1] - (s[Store_D,0] - sls[Store_D,0]) : 0.0 : True + ('Store_D', 1) : 0.0 : s[Store_D,2] - (s[Store_D,1] - sls[Store_D,1] + q[DC2,Store_D,0] + q[DC3,Store_D,0]) : 0.0 : True + ('Store_D', 2) : 0.0 : s[Store_D,3] - (s[Store_D,2] - sls[Store_D,2] + q[DC1,Store_D,0] + q[DC2,Store_D,1] + q[DC3,Store_D,1]) : 0.0 : True + ('Store_D', 3) : 0.0 : s[Store_D,4] - (s[Store_D,3] - sls[Store_D,3] + q[DC1,Store_D,1] + q[DC2,Store_D,2] + q[DC3,Store_D,2]) : 0.0 : True + ('Store_D', 4) : 0.0 : s[Store_D,5] - (s[Store_D,4] - sls[Store_D,4] + q[DC1,Store_D,2] + q[DC2,Store_D,3] + q[DC3,Store_D,3]) : 0.0 : True + ('Store_D', 5) : 0.0 : s[Store_D,6] - (s[Store_D,5] - sls[Store_D,5] + q[DC1,Store_D,3] + q[DC2,Store_D,4] + q[DC3,Store_D,4]) : 0.0 : True + recipient_need : Recipient need constraint: sum over i of q[i,j,t] <= need[j,t] for all j in R, t + Size=28, Index=R*T, Active=True + Key : Lower : Body : Upper : Active + ('Store_A', 0) : -Inf : q[DC1,Store_A,0] + q[DC2,Store_A,0] + q[DC3,Store_A,0] : need[Store_A,0] : True + ('Store_A', 1) : -Inf : q[DC1,Store_A,1] + q[DC2,Store_A,1] + q[DC3,Store_A,1] : need[Store_A,1] : True + ('Store_A', 2) : -Inf : q[DC1,Store_A,2] + q[DC2,Store_A,2] + q[DC3,Store_A,2] : need[Store_A,2] : True + ('Store_A', 3) : -Inf : q[DC1,Store_A,3] + q[DC2,Store_A,3] + q[DC3,Store_A,3] : need[Store_A,3] : True + ('Store_A', 4) : -Inf : q[DC1,Store_A,4] + q[DC2,Store_A,4] + q[DC3,Store_A,4] : need[Store_A,4] : True + ('Store_A', 5) : -Inf : q[DC1,Store_A,5] + q[DC2,Store_A,5] + q[DC3,Store_A,5] : need[Store_A,5] : True + ('Store_A', 6) : -Inf : q[DC1,Store_A,6] + q[DC2,Store_A,6] + q[DC3,Store_A,6] : need[Store_A,6] : True + ('Store_B', 0) : -Inf : q[DC1,Store_B,0] + q[DC2,Store_B,0] + q[DC3,Store_B,0] : need[Store_B,0] : True + ('Store_B', 1) : -Inf : q[DC1,Store_B,1] + q[DC2,Store_B,1] + q[DC3,Store_B,1] : need[Store_B,1] : True + ('Store_B', 2) : -Inf : q[DC1,Store_B,2] + q[DC2,Store_B,2] + q[DC3,Store_B,2] : need[Store_B,2] : True + ('Store_B', 3) : -Inf : q[DC1,Store_B,3] + q[DC2,Store_B,3] + q[DC3,Store_B,3] : need[Store_B,3] : True + ('Store_B', 4) : -Inf : q[DC1,Store_B,4] + q[DC2,Store_B,4] + q[DC3,Store_B,4] : need[Store_B,4] : True + ('Store_B', 5) : -Inf : q[DC1,Store_B,5] + q[DC2,Store_B,5] + q[DC3,Store_B,5] : need[Store_B,5] : True + ('Store_B', 6) : -Inf : q[DC1,Store_B,6] + q[DC2,Store_B,6] + q[DC3,Store_B,6] : need[Store_B,6] : True + ('Store_C', 0) : -Inf : q[DC1,Store_C,0] + q[DC2,Store_C,0] + q[DC3,Store_C,0] : need[Store_C,0] : True + ('Store_C', 1) : -Inf : q[DC1,Store_C,1] + q[DC2,Store_C,1] + q[DC3,Store_C,1] : need[Store_C,1] : True + ('Store_C', 2) : -Inf : q[DC1,Store_C,2] + q[DC2,Store_C,2] + q[DC3,Store_C,2] : need[Store_C,2] : True + ('Store_C', 3) : -Inf : q[DC1,Store_C,3] + q[DC2,Store_C,3] + q[DC3,Store_C,3] : need[Store_C,3] : True + ('Store_C', 4) : -Inf : q[DC1,Store_C,4] + q[DC2,Store_C,4] + q[DC3,Store_C,4] : need[Store_C,4] : True + ('Store_C', 5) : -Inf : q[DC1,Store_C,5] + q[DC2,Store_C,5] + q[DC3,Store_C,5] : need[Store_C,5] : True + ('Store_C', 6) : -Inf : q[DC1,Store_C,6] + q[DC2,Store_C,6] + q[DC3,Store_C,6] : need[Store_C,6] : True + ('Store_D', 0) : -Inf : q[DC1,Store_D,0] + q[DC2,Store_D,0] + q[DC3,Store_D,0] : need[Store_D,0] : True + ('Store_D', 1) : -Inf : q[DC1,Store_D,1] + q[DC2,Store_D,1] + q[DC3,Store_D,1] : need[Store_D,1] : True + ('Store_D', 2) : -Inf : q[DC1,Store_D,2] + q[DC2,Store_D,2] + q[DC3,Store_D,2] : need[Store_D,2] : True + ('Store_D', 3) : -Inf : q[DC1,Store_D,3] + q[DC2,Store_D,3] + q[DC3,Store_D,3] : need[Store_D,3] : True + ('Store_D', 4) : -Inf : q[DC1,Store_D,4] + q[DC2,Store_D,4] + q[DC3,Store_D,4] : need[Store_D,4] : True + ('Store_D', 5) : -Inf : q[DC1,Store_D,5] + q[DC2,Store_D,5] + q[DC3,Store_D,5] : need[Store_D,5] : True + ('Store_D', 6) : -Inf : q[DC1,Store_D,6] + q[DC2,Store_D,6] + q[DC3,Store_D,6] : need[Store_D,6] : True + rutl_constraint : RUTL constraint: s[j,t] + sum over i of q[i,j,t] <= RUTL[j,t] for all j in R, t + Size=28, Index=R*T, Active=True + Key : Lower : Body : Upper : Active + ('Store_A', 0) : -Inf : q[DC1,Store_A,0] + q[DC2,Store_A,0] + q[DC3,Store_A,0] + s[Store_A,0] : RUTL[Store_A,0] : True + ('Store_A', 1) : -Inf : q[DC1,Store_A,1] + q[DC2,Store_A,1] + q[DC3,Store_A,1] + s[Store_A,1] : RUTL[Store_A,1] : True + ('Store_A', 2) : -Inf : q[DC1,Store_A,2] + q[DC2,Store_A,2] + q[DC3,Store_A,2] + s[Store_A,2] : RUTL[Store_A,2] : True + ('Store_A', 3) : -Inf : q[DC1,Store_A,3] + q[DC2,Store_A,3] + q[DC3,Store_A,3] + s[Store_A,3] : RUTL[Store_A,3] : True + ('Store_A', 4) : -Inf : q[DC1,Store_A,4] + q[DC2,Store_A,4] + q[DC3,Store_A,4] + s[Store_A,4] : RUTL[Store_A,4] : True + ('Store_A', 5) : -Inf : q[DC1,Store_A,5] + q[DC2,Store_A,5] + q[DC3,Store_A,5] + s[Store_A,5] : RUTL[Store_A,5] : True + ('Store_A', 6) : -Inf : q[DC1,Store_A,6] + q[DC2,Store_A,6] + q[DC3,Store_A,6] + s[Store_A,6] : RUTL[Store_A,6] : True + ('Store_B', 0) : -Inf : q[DC1,Store_B,0] + q[DC2,Store_B,0] + q[DC3,Store_B,0] + s[Store_B,0] : RUTL[Store_B,0] : True + ('Store_B', 1) : -Inf : q[DC1,Store_B,1] + q[DC2,Store_B,1] + q[DC3,Store_B,1] + s[Store_B,1] : RUTL[Store_B,1] : True + ('Store_B', 2) : -Inf : q[DC1,Store_B,2] + q[DC2,Store_B,2] + q[DC3,Store_B,2] + s[Store_B,2] : RUTL[Store_B,2] : True + ('Store_B', 3) : -Inf : q[DC1,Store_B,3] + q[DC2,Store_B,3] + q[DC3,Store_B,3] + s[Store_B,3] : RUTL[Store_B,3] : True + ('Store_B', 4) : -Inf : q[DC1,Store_B,4] + q[DC2,Store_B,4] + q[DC3,Store_B,4] + s[Store_B,4] : RUTL[Store_B,4] : True + ('Store_B', 5) : -Inf : q[DC1,Store_B,5] + q[DC2,Store_B,5] + q[DC3,Store_B,5] + s[Store_B,5] : RUTL[Store_B,5] : True + ('Store_B', 6) : -Inf : q[DC1,Store_B,6] + q[DC2,Store_B,6] + q[DC3,Store_B,6] + s[Store_B,6] : RUTL[Store_B,6] : True + ('Store_C', 0) : -Inf : q[DC1,Store_C,0] + q[DC2,Store_C,0] + q[DC3,Store_C,0] + s[Store_C,0] : RUTL[Store_C,0] : True + ('Store_C', 1) : -Inf : q[DC1,Store_C,1] + q[DC2,Store_C,1] + q[DC3,Store_C,1] + s[Store_C,1] : RUTL[Store_C,1] : True + ('Store_C', 2) : -Inf : q[DC1,Store_C,2] + q[DC2,Store_C,2] + q[DC3,Store_C,2] + s[Store_C,2] : RUTL[Store_C,2] : True + ('Store_C', 3) : -Inf : q[DC1,Store_C,3] + q[DC2,Store_C,3] + q[DC3,Store_C,3] + s[Store_C,3] : RUTL[Store_C,3] : True + ('Store_C', 4) : -Inf : q[DC1,Store_C,4] + q[DC2,Store_C,4] + q[DC3,Store_C,4] + s[Store_C,4] : RUTL[Store_C,4] : True + ('Store_C', 5) : -Inf : q[DC1,Store_C,5] + q[DC2,Store_C,5] + q[DC3,Store_C,5] + s[Store_C,5] : RUTL[Store_C,5] : True + ('Store_C', 6) : -Inf : q[DC1,Store_C,6] + q[DC2,Store_C,6] + q[DC3,Store_C,6] + s[Store_C,6] : RUTL[Store_C,6] : True + ('Store_D', 0) : -Inf : q[DC1,Store_D,0] + q[DC2,Store_D,0] + q[DC3,Store_D,0] + s[Store_D,0] : RUTL[Store_D,0] : True + ('Store_D', 1) : -Inf : q[DC1,Store_D,1] + q[DC2,Store_D,1] + q[DC3,Store_D,1] + s[Store_D,1] : RUTL[Store_D,1] : True + ('Store_D', 2) : -Inf : q[DC1,Store_D,2] + q[DC2,Store_D,2] + q[DC3,Store_D,2] + s[Store_D,2] : RUTL[Store_D,2] : True + ('Store_D', 3) : -Inf : q[DC1,Store_D,3] + q[DC2,Store_D,3] + q[DC3,Store_D,3] + s[Store_D,3] : RUTL[Store_D,3] : True + ('Store_D', 4) : -Inf : q[DC1,Store_D,4] + q[DC2,Store_D,4] + q[DC3,Store_D,4] + s[Store_D,4] : RUTL[Store_D,4] : True + ('Store_D', 5) : -Inf : q[DC1,Store_D,5] + q[DC2,Store_D,5] + q[DC3,Store_D,5] + s[Store_D,5] : RUTL[Store_D,5] : True + ('Store_D', 6) : -Inf : q[DC1,Store_D,6] + q[DC2,Store_D,6] + q[DC3,Store_D,6] + s[Store_D,6] : RUTL[Store_D,6] : True + sales_upper1 : Sales constraint: sls[j,t] = s[j,t] for all j in R, t + Size=28, Index=R*T, Active=True + Key : Lower : Body : Upper : Active + ('Store_A', 0) : -Inf : sls[Store_A,0] - s[Store_A,0] : 0.0 : True + ('Store_A', 1) : -Inf : sls[Store_A,1] - s[Store_A,1] : 0.0 : True + ('Store_A', 2) : -Inf : sls[Store_A,2] - s[Store_A,2] : 0.0 : True + ('Store_A', 3) : -Inf : sls[Store_A,3] - s[Store_A,3] : 0.0 : True + ('Store_A', 4) : -Inf : sls[Store_A,4] - s[Store_A,4] : 0.0 : True + ('Store_A', 5) : -Inf : sls[Store_A,5] - s[Store_A,5] : 0.0 : True + ('Store_A', 6) : -Inf : sls[Store_A,6] - s[Store_A,6] : 0.0 : True + ('Store_B', 0) : -Inf : sls[Store_B,0] - s[Store_B,0] : 0.0 : True + ('Store_B', 1) : -Inf : sls[Store_B,1] - s[Store_B,1] : 0.0 : True + ('Store_B', 2) : -Inf : sls[Store_B,2] - s[Store_B,2] : 0.0 : True + ('Store_B', 3) : -Inf : sls[Store_B,3] - s[Store_B,3] : 0.0 : True + ('Store_B', 4) : -Inf : sls[Store_B,4] - s[Store_B,4] : 0.0 : True + ('Store_B', 5) : -Inf : sls[Store_B,5] - s[Store_B,5] : 0.0 : True + ('Store_B', 6) : -Inf : sls[Store_B,6] - s[Store_B,6] : 0.0 : True + ('Store_C', 0) : -Inf : sls[Store_C,0] - s[Store_C,0] : 0.0 : True + ('Store_C', 1) : -Inf : sls[Store_C,1] - s[Store_C,1] : 0.0 : True + ('Store_C', 2) : -Inf : sls[Store_C,2] - s[Store_C,2] : 0.0 : True + ('Store_C', 3) : -Inf : sls[Store_C,3] - s[Store_C,3] : 0.0 : True + ('Store_C', 4) : -Inf : sls[Store_C,4] - s[Store_C,4] : 0.0 : True + ('Store_C', 5) : -Inf : sls[Store_C,5] - s[Store_C,5] : 0.0 : True + ('Store_C', 6) : -Inf : sls[Store_C,6] - s[Store_C,6] : 0.0 : True + ('Store_D', 0) : -Inf : sls[Store_D,0] - s[Store_D,0] : 0.0 : True + ('Store_D', 1) : -Inf : sls[Store_D,1] - s[Store_D,1] : 0.0 : True + ('Store_D', 2) : -Inf : sls[Store_D,2] - s[Store_D,2] : 0.0 : True + ('Store_D', 3) : -Inf : sls[Store_D,3] - s[Store_D,3] : 0.0 : True + ('Store_D', 4) : -Inf : sls[Store_D,4] - s[Store_D,4] : 0.0 : True + ('Store_D', 5) : -Inf : sls[Store_D,5] - s[Store_D,5] : 0.0 : True + ('Store_D', 6) : -Inf : sls[Store_D,6] - s[Store_D,6] : 0.0 : True + sales_upper2 : Sales constraint: sls[j,t] <= d[j,t] for all j in R, t + Size=28, Index=R*T, Active=True + Key : Lower : Body : Upper : Active + ('Store_A', 0) : -Inf : sls[Store_A,0] : d[Store_A,0] : True + ('Store_A', 1) : -Inf : sls[Store_A,1] : d[Store_A,1] : True + ('Store_A', 2) : -Inf : sls[Store_A,2] : d[Store_A,2] : True + ('Store_A', 3) : -Inf : sls[Store_A,3] : d[Store_A,3] : True + ('Store_A', 4) : -Inf : sls[Store_A,4] : d[Store_A,4] : True + ('Store_A', 5) : -Inf : sls[Store_A,5] : d[Store_A,5] : True + ('Store_A', 6) : -Inf : sls[Store_A,6] : d[Store_A,6] : True + ('Store_B', 0) : -Inf : sls[Store_B,0] : d[Store_B,0] : True + ('Store_B', 1) : -Inf : sls[Store_B,1] : d[Store_B,1] : True + ('Store_B', 2) : -Inf : sls[Store_B,2] : d[Store_B,2] : True + ('Store_B', 3) : -Inf : sls[Store_B,3] : d[Store_B,3] : True + ('Store_B', 4) : -Inf : sls[Store_B,4] : d[Store_B,4] : True + ('Store_B', 5) : -Inf : sls[Store_B,5] : d[Store_B,5] : True + ('Store_B', 6) : -Inf : sls[Store_B,6] : d[Store_B,6] : True + ('Store_C', 0) : -Inf : sls[Store_C,0] : d[Store_C,0] : True + ('Store_C', 1) : -Inf : sls[Store_C,1] : d[Store_C,1] : True + ('Store_C', 2) : -Inf : sls[Store_C,2] : d[Store_C,2] : True + ('Store_C', 3) : -Inf : sls[Store_C,3] : d[Store_C,3] : True + ('Store_C', 4) : -Inf : sls[Store_C,4] : d[Store_C,4] : True + ('Store_C', 5) : -Inf : sls[Store_C,5] : d[Store_C,5] : True + ('Store_C', 6) : -Inf : sls[Store_C,6] : d[Store_C,6] : True + ('Store_D', 0) : -Inf : sls[Store_D,0] : d[Store_D,0] : True + ('Store_D', 1) : -Inf : sls[Store_D,1] : d[Store_D,1] : True + ('Store_D', 2) : -Inf : sls[Store_D,2] : d[Store_D,2] : True + ('Store_D', 3) : -Inf : sls[Store_D,3] : d[Store_D,3] : True + ('Store_D', 4) : -Inf : sls[Store_D,4] : d[Store_D,4] : True + ('Store_D', 5) : -Inf : sls[Store_D,5] : d[Store_D,5] : True + ('Store_D', 6) : -Inf : sls[Store_D,6] : d[Store_D,6] : True + sender_capacity : Sender capacity constraint: sum over j,t of q[i,j,t] <= s[i,t] for all i in S, t + Size=21, Index=S*T, Active=True + Key : Lower : Body : Upper : Active + ('DC1', 0) : -Inf : q[DC1,Store_A,0] + q[DC1,Store_B,0] + q[DC1,Store_C,0] + q[DC1,Store_D,0] - s[DC1,0] : 0.0 : True + ('DC1', 1) : -Inf : q[DC1,Store_A,1] + q[DC1,Store_B,1] + q[DC1,Store_C,1] + q[DC1,Store_D,1] - s[DC1,1] : 0.0 : True + ('DC1', 2) : -Inf : q[DC1,Store_A,2] + q[DC1,Store_B,2] + q[DC1,Store_C,2] + q[DC1,Store_D,2] - s[DC1,2] : 0.0 : True + ('DC1', 3) : -Inf : q[DC1,Store_A,3] + q[DC1,Store_B,3] + q[DC1,Store_C,3] + q[DC1,Store_D,3] - s[DC1,3] : 0.0 : True + ('DC1', 4) : -Inf : q[DC1,Store_A,4] + q[DC1,Store_B,4] + q[DC1,Store_C,4] + q[DC1,Store_D,4] - s[DC1,4] : 0.0 : True + ('DC1', 5) : -Inf : q[DC1,Store_A,5] + q[DC1,Store_B,5] + q[DC1,Store_C,5] + q[DC1,Store_D,5] - s[DC1,5] : 0.0 : True + ('DC1', 6) : -Inf : q[DC1,Store_A,6] + q[DC1,Store_B,6] + q[DC1,Store_C,6] + q[DC1,Store_D,6] - s[DC1,6] : 0.0 : True + ('DC2', 0) : -Inf : q[DC2,Store_A,0] + q[DC2,Store_B,0] + q[DC2,Store_C,0] + q[DC2,Store_D,0] - s[DC2,0] : 0.0 : True + ('DC2', 1) : -Inf : q[DC2,Store_A,1] + q[DC2,Store_B,1] + q[DC2,Store_C,1] + q[DC2,Store_D,1] - s[DC2,1] : 0.0 : True + ('DC2', 2) : -Inf : q[DC2,Store_A,2] + q[DC2,Store_B,2] + q[DC2,Store_C,2] + q[DC2,Store_D,2] - s[DC2,2] : 0.0 : True + ('DC2', 3) : -Inf : q[DC2,Store_A,3] + q[DC2,Store_B,3] + q[DC2,Store_C,3] + q[DC2,Store_D,3] - s[DC2,3] : 0.0 : True + ('DC2', 4) : -Inf : q[DC2,Store_A,4] + q[DC2,Store_B,4] + q[DC2,Store_C,4] + q[DC2,Store_D,4] - s[DC2,4] : 0.0 : True + ('DC2', 5) : -Inf : q[DC2,Store_A,5] + q[DC2,Store_B,5] + q[DC2,Store_C,5] + q[DC2,Store_D,5] - s[DC2,5] : 0.0 : True + ('DC2', 6) : -Inf : q[DC2,Store_A,6] + q[DC2,Store_B,6] + q[DC2,Store_C,6] + q[DC2,Store_D,6] - s[DC2,6] : 0.0 : True + ('DC3', 0) : -Inf : q[DC3,Store_A,0] + q[DC3,Store_B,0] + q[DC3,Store_C,0] + q[DC3,Store_D,0] - s[DC3,0] : 0.0 : True + ('DC3', 1) : -Inf : q[DC3,Store_A,1] + q[DC3,Store_B,1] + q[DC3,Store_C,1] + q[DC3,Store_D,1] - s[DC3,1] : 0.0 : True + ('DC3', 2) : -Inf : q[DC3,Store_A,2] + q[DC3,Store_B,2] + q[DC3,Store_C,2] + q[DC3,Store_D,2] - s[DC3,2] : 0.0 : True + ('DC3', 3) : -Inf : q[DC3,Store_A,3] + q[DC3,Store_B,3] + q[DC3,Store_C,3] + q[DC3,Store_D,3] - s[DC3,3] : 0.0 : True + ('DC3', 4) : -Inf : q[DC3,Store_A,4] + q[DC3,Store_B,4] + q[DC3,Store_C,4] + q[DC3,Store_D,4] - s[DC3,4] : 0.0 : True + ('DC3', 5) : -Inf : q[DC3,Store_A,5] + q[DC3,Store_B,5] + q[DC3,Store_C,5] + q[DC3,Store_D,5] - s[DC3,5] : 0.0 : True + ('DC3', 6) : -Inf : q[DC3,Store_A,6] + q[DC3,Store_B,6] + q[DC3,Store_C,6] + q[DC3,Store_D,6] - s[DC3,6] : 0.0 : True + sender_inventory_update : Sender inventory update: s[i,t+1] = s[i,t] - sum over j of q[i,j,t] for all i in S, t + Size=18, Index=S*T, Active=True + Key : Lower : Body : Upper : Active + ('DC1', 0) : 0.0 : s[DC1,1] - (s[DC1,0] - (q[DC1,Store_A,0] + q[DC1,Store_B,0] + q[DC1,Store_C,0] + q[DC1,Store_D,0])) : 0.0 : True + ('DC1', 1) : 0.0 : s[DC1,2] - (s[DC1,1] - (q[DC1,Store_A,1] + q[DC1,Store_B,1] + q[DC1,Store_C,1] + q[DC1,Store_D,1])) : 0.0 : True + ('DC1', 2) : 0.0 : s[DC1,3] - (s[DC1,2] - (q[DC1,Store_A,2] + q[DC1,Store_B,2] + q[DC1,Store_C,2] + q[DC1,Store_D,2])) : 0.0 : True + ('DC1', 3) : 0.0 : s[DC1,4] - (s[DC1,3] - (q[DC1,Store_A,3] + q[DC1,Store_B,3] + q[DC1,Store_C,3] + q[DC1,Store_D,3])) : 0.0 : True + ('DC1', 4) : 0.0 : s[DC1,5] - (s[DC1,4] - (q[DC1,Store_A,4] + q[DC1,Store_B,4] + q[DC1,Store_C,4] + q[DC1,Store_D,4])) : 0.0 : True + ('DC1', 5) : 0.0 : s[DC1,6] - (s[DC1,5] - (q[DC1,Store_A,5] + q[DC1,Store_B,5] + q[DC1,Store_C,5] + q[DC1,Store_D,5])) : 0.0 : True + ('DC2', 0) : 0.0 : s[DC2,1] - (s[DC2,0] - (q[DC2,Store_A,0] + q[DC2,Store_B,0] + q[DC2,Store_C,0] + q[DC2,Store_D,0])) : 0.0 : True + ('DC2', 1) : 0.0 : s[DC2,2] - (s[DC2,1] - (q[DC2,Store_A,1] + q[DC2,Store_B,1] + q[DC2,Store_C,1] + q[DC2,Store_D,1])) : 0.0 : True + ('DC2', 2) : 0.0 : s[DC2,3] - (s[DC2,2] - (q[DC2,Store_A,2] + q[DC2,Store_B,2] + q[DC2,Store_C,2] + q[DC2,Store_D,2])) : 0.0 : True + ('DC2', 3) : 0.0 : s[DC2,4] - (s[DC2,3] - (q[DC2,Store_A,3] + q[DC2,Store_B,3] + q[DC2,Store_C,3] + q[DC2,Store_D,3])) : 0.0 : True + ('DC2', 4) : 0.0 : s[DC2,5] - (s[DC2,4] - (q[DC2,Store_A,4] + q[DC2,Store_B,4] + q[DC2,Store_C,4] + q[DC2,Store_D,4])) : 0.0 : True + ('DC2', 5) : 0.0 : s[DC2,6] - (s[DC2,5] - (q[DC2,Store_A,5] + q[DC2,Store_B,5] + q[DC2,Store_C,5] + q[DC2,Store_D,5])) : 0.0 : True + ('DC3', 0) : 0.0 : s[DC3,1] - (s[DC3,0] - (q[DC3,Store_A,0] + q[DC3,Store_B,0] + q[DC3,Store_C,0] + q[DC3,Store_D,0])) : 0.0 : True + ('DC3', 1) : 0.0 : s[DC3,2] - (s[DC3,1] - (q[DC3,Store_A,1] + q[DC3,Store_B,1] + q[DC3,Store_C,1] + q[DC3,Store_D,1])) : 0.0 : True + ('DC3', 2) : 0.0 : s[DC3,3] - (s[DC3,2] - (q[DC3,Store_A,2] + q[DC3,Store_B,2] + q[DC3,Store_C,2] + q[DC3,Store_D,2])) : 0.0 : True + ('DC3', 3) : 0.0 : s[DC3,4] - (s[DC3,3] - (q[DC3,Store_A,3] + q[DC3,Store_B,3] + q[DC3,Store_C,3] + q[DC3,Store_D,3])) : 0.0 : True + ('DC3', 4) : 0.0 : s[DC3,5] - (s[DC3,4] - (q[DC3,Store_A,4] + q[DC3,Store_B,4] + q[DC3,Store_C,4] + q[DC3,Store_D,4])) : 0.0 : True + ('DC3', 5) : 0.0 : s[DC3,6] - (s[DC3,5] - (q[DC3,Store_A,5] + q[DC3,Store_B,5] + q[DC3,Store_C,5] + q[DC3,Store_D,5])) : 0.0 : True + u_minus_constraint : U_minus constraint: U_minus >= aim[j,t] - sum over i of q[i,j,t] for all j in R, t + Size=28, Index=R*T, Active=True + Key : Lower : Body : Upper : Active + ('Store_A', 0) : -Inf : aim[Store_A,0] - (q[DC1,Store_A,0] + q[DC2,Store_A,0] + q[DC3,Store_A,0]) - U_minus : 0.0 : True + ('Store_A', 1) : -Inf : aim[Store_A,1] - (q[DC1,Store_A,1] + q[DC2,Store_A,1] + q[DC3,Store_A,1]) - U_minus : 0.0 : True + ('Store_A', 2) : -Inf : aim[Store_A,2] - (q[DC1,Store_A,2] + q[DC2,Store_A,2] + q[DC3,Store_A,2]) - U_minus : 0.0 : True + ('Store_A', 3) : -Inf : aim[Store_A,3] - (q[DC1,Store_A,3] + q[DC2,Store_A,3] + q[DC3,Store_A,3]) - U_minus : 0.0 : True + ('Store_A', 4) : -Inf : aim[Store_A,4] - (q[DC1,Store_A,4] + q[DC2,Store_A,4] + q[DC3,Store_A,4]) - U_minus : 0.0 : True + ('Store_A', 5) : -Inf : aim[Store_A,5] - (q[DC1,Store_A,5] + q[DC2,Store_A,5] + q[DC3,Store_A,5]) - U_minus : 0.0 : True + ('Store_A', 6) : -Inf : aim[Store_A,6] - (q[DC1,Store_A,6] + q[DC2,Store_A,6] + q[DC3,Store_A,6]) - U_minus : 0.0 : True + ('Store_B', 0) : -Inf : aim[Store_B,0] - (q[DC1,Store_B,0] + q[DC2,Store_B,0] + q[DC3,Store_B,0]) - U_minus : 0.0 : True + ('Store_B', 1) : -Inf : aim[Store_B,1] - (q[DC1,Store_B,1] + q[DC2,Store_B,1] + q[DC3,Store_B,1]) - U_minus : 0.0 : True + ('Store_B', 2) : -Inf : aim[Store_B,2] - (q[DC1,Store_B,2] + q[DC2,Store_B,2] + q[DC3,Store_B,2]) - U_minus : 0.0 : True + ('Store_B', 3) : -Inf : aim[Store_B,3] - (q[DC1,Store_B,3] + q[DC2,Store_B,3] + q[DC3,Store_B,3]) - U_minus : 0.0 : True + ('Store_B', 4) : -Inf : aim[Store_B,4] - (q[DC1,Store_B,4] + q[DC2,Store_B,4] + q[DC3,Store_B,4]) - U_minus : 0.0 : True + ('Store_B', 5) : -Inf : aim[Store_B,5] - (q[DC1,Store_B,5] + q[DC2,Store_B,5] + q[DC3,Store_B,5]) - U_minus : 0.0 : True + ('Store_B', 6) : -Inf : aim[Store_B,6] - (q[DC1,Store_B,6] + q[DC2,Store_B,6] + q[DC3,Store_B,6]) - U_minus : 0.0 : True + ('Store_C', 0) : -Inf : aim[Store_C,0] - (q[DC1,Store_C,0] + q[DC2,Store_C,0] + q[DC3,Store_C,0]) - U_minus : 0.0 : True + ('Store_C', 1) : -Inf : aim[Store_C,1] - (q[DC1,Store_C,1] + q[DC2,Store_C,1] + q[DC3,Store_C,1]) - U_minus : 0.0 : True + ('Store_C', 2) : -Inf : aim[Store_C,2] - (q[DC1,Store_C,2] + q[DC2,Store_C,2] + q[DC3,Store_C,2]) - U_minus : 0.0 : True + ('Store_C', 3) : -Inf : aim[Store_C,3] - (q[DC1,Store_C,3] + q[DC2,Store_C,3] + q[DC3,Store_C,3]) - U_minus : 0.0 : True + ('Store_C', 4) : -Inf : aim[Store_C,4] - (q[DC1,Store_C,4] + q[DC2,Store_C,4] + q[DC3,Store_C,4]) - U_minus : 0.0 : True + ('Store_C', 5) : -Inf : aim[Store_C,5] - (q[DC1,Store_C,5] + q[DC2,Store_C,5] + q[DC3,Store_C,5]) - U_minus : 0.0 : True + ('Store_C', 6) : -Inf : aim[Store_C,6] - (q[DC1,Store_C,6] + q[DC2,Store_C,6] + q[DC3,Store_C,6]) - U_minus : 0.0 : True + ('Store_D', 0) : -Inf : aim[Store_D,0] - (q[DC1,Store_D,0] + q[DC2,Store_D,0] + q[DC3,Store_D,0]) - U_minus : 0.0 : True + ('Store_D', 1) : -Inf : aim[Store_D,1] - (q[DC1,Store_D,1] + q[DC2,Store_D,1] + q[DC3,Store_D,1]) - U_minus : 0.0 : True + ('Store_D', 2) : -Inf : aim[Store_D,2] - (q[DC1,Store_D,2] + q[DC2,Store_D,2] + q[DC3,Store_D,2]) - U_minus : 0.0 : True + ('Store_D', 3) : -Inf : aim[Store_D,3] - (q[DC1,Store_D,3] + q[DC2,Store_D,3] + q[DC3,Store_D,3]) - U_minus : 0.0 : True + ('Store_D', 4) : -Inf : aim[Store_D,4] - (q[DC1,Store_D,4] + q[DC2,Store_D,4] + q[DC3,Store_D,4]) - U_minus : 0.0 : True + ('Store_D', 5) : -Inf : aim[Store_D,5] - (q[DC1,Store_D,5] + q[DC2,Store_D,5] + q[DC3,Store_D,5]) - U_minus : 0.0 : True + ('Store_D', 6) : -Inf : aim[Store_D,6] - (q[DC1,Store_D,6] + q[DC2,Store_D,6] + q[DC3,Store_D,6]) - U_minus : 0.0 : True + u_plus_constraint : U_plus constraint: U_plus >= sum over i of q[i,j,t] - aim[j,t] for all j in R, t + Size=28, Index=R*T, Active=True + Key : Lower : Body : Upper : Active + ('Store_A', 0) : -Inf : q[DC1,Store_A,0] + q[DC2,Store_A,0] + q[DC3,Store_A,0] - aim[Store_A,0] - U_plus : 0.0 : True + ('Store_A', 1) : -Inf : q[DC1,Store_A,1] + q[DC2,Store_A,1] + q[DC3,Store_A,1] - aim[Store_A,1] - U_plus : 0.0 : True + ('Store_A', 2) : -Inf : q[DC1,Store_A,2] + q[DC2,Store_A,2] + q[DC3,Store_A,2] - aim[Store_A,2] - U_plus : 0.0 : True + ('Store_A', 3) : -Inf : q[DC1,Store_A,3] + q[DC2,Store_A,3] + q[DC3,Store_A,3] - aim[Store_A,3] - U_plus : 0.0 : True + ('Store_A', 4) : -Inf : q[DC1,Store_A,4] + q[DC2,Store_A,4] + q[DC3,Store_A,4] - aim[Store_A,4] - U_plus : 0.0 : True + ('Store_A', 5) : -Inf : q[DC1,Store_A,5] + q[DC2,Store_A,5] + q[DC3,Store_A,5] - aim[Store_A,5] - U_plus : 0.0 : True + ('Store_A', 6) : -Inf : q[DC1,Store_A,6] + q[DC2,Store_A,6] + q[DC3,Store_A,6] - aim[Store_A,6] - U_plus : 0.0 : True + ('Store_B', 0) : -Inf : q[DC1,Store_B,0] + q[DC2,Store_B,0] + q[DC3,Store_B,0] - aim[Store_B,0] - U_plus : 0.0 : True + ('Store_B', 1) : -Inf : q[DC1,Store_B,1] + q[DC2,Store_B,1] + q[DC3,Store_B,1] - aim[Store_B,1] - U_plus : 0.0 : True + ('Store_B', 2) : -Inf : q[DC1,Store_B,2] + q[DC2,Store_B,2] + q[DC3,Store_B,2] - aim[Store_B,2] - U_plus : 0.0 : True + ('Store_B', 3) : -Inf : q[DC1,Store_B,3] + q[DC2,Store_B,3] + q[DC3,Store_B,3] - aim[Store_B,3] - U_plus : 0.0 : True + ('Store_B', 4) : -Inf : q[DC1,Store_B,4] + q[DC2,Store_B,4] + q[DC3,Store_B,4] - aim[Store_B,4] - U_plus : 0.0 : True + ('Store_B', 5) : -Inf : q[DC1,Store_B,5] + q[DC2,Store_B,5] + q[DC3,Store_B,5] - aim[Store_B,5] - U_plus : 0.0 : True + ('Store_B', 6) : -Inf : q[DC1,Store_B,6] + q[DC2,Store_B,6] + q[DC3,Store_B,6] - aim[Store_B,6] - U_plus : 0.0 : True + ('Store_C', 0) : -Inf : q[DC1,Store_C,0] + q[DC2,Store_C,0] + q[DC3,Store_C,0] - aim[Store_C,0] - U_plus : 0.0 : True + ('Store_C', 1) : -Inf : q[DC1,Store_C,1] + q[DC2,Store_C,1] + q[DC3,Store_C,1] - aim[Store_C,1] - U_plus : 0.0 : True + ('Store_C', 2) : -Inf : q[DC1,Store_C,2] + q[DC2,Store_C,2] + q[DC3,Store_C,2] - aim[Store_C,2] - U_plus : 0.0 : True + ('Store_C', 3) : -Inf : q[DC1,Store_C,3] + q[DC2,Store_C,3] + q[DC3,Store_C,3] - aim[Store_C,3] - U_plus : 0.0 : True + ('Store_C', 4) : -Inf : q[DC1,Store_C,4] + q[DC2,Store_C,4] + q[DC3,Store_C,4] - aim[Store_C,4] - U_plus : 0.0 : True + ('Store_C', 5) : -Inf : q[DC1,Store_C,5] + q[DC2,Store_C,5] + q[DC3,Store_C,5] - aim[Store_C,5] - U_plus : 0.0 : True + ('Store_C', 6) : -Inf : q[DC1,Store_C,6] + q[DC2,Store_C,6] + q[DC3,Store_C,6] - aim[Store_C,6] - U_plus : 0.0 : True + ('Store_D', 0) : -Inf : q[DC1,Store_D,0] + q[DC2,Store_D,0] + q[DC3,Store_D,0] - aim[Store_D,0] - U_plus : 0.0 : True + ('Store_D', 1) : -Inf : q[DC1,Store_D,1] + q[DC2,Store_D,1] + q[DC3,Store_D,1] - aim[Store_D,1] - U_plus : 0.0 : True + ('Store_D', 2) : -Inf : q[DC1,Store_D,2] + q[DC2,Store_D,2] + q[DC3,Store_D,2] - aim[Store_D,2] - U_plus : 0.0 : True + ('Store_D', 3) : -Inf : q[DC1,Store_D,3] + q[DC2,Store_D,3] + q[DC3,Store_D,3] - aim[Store_D,3] - U_plus : 0.0 : True + ('Store_D', 4) : -Inf : q[DC1,Store_D,4] + q[DC2,Store_D,4] + q[DC3,Store_D,4] - aim[Store_D,4] - U_plus : 0.0 : True + ('Store_D', 5) : -Inf : q[DC1,Store_D,5] + q[DC2,Store_D,5] + q[DC3,Store_D,5] - aim[Store_D,5] - U_plus : 0.0 : True + ('Store_D', 6) : -Inf : q[DC1,Store_D,6] + q[DC2,Store_D,6] + q[DC3,Store_D,6] - aim[Store_D,6] - U_plus : 0.0 : True + +39 Declarations: S R T T_plus c s0_i s0_j need aim M RP RUTL d LT need_min_param U_plus U_minus q x s NI sls obj need_min sender_capacity recipient_need linking u_minus_constraint u_plus_constraint init_sender_inventory init_recipient_inventory sender_inventory_update big_m_constraint rutl_constraint sales_upper1 sales_upper2 recipient_inventory_update net_inventory_update inventory_capacity diff --git a/debug_use/allocation_model_infeasible_unsolved_cfg.json b/debug_use/allocation_model_infeasible_unsolved_cfg.json new file mode 100644 index 0000000..c6564b0 --- /dev/null +++ b/debug_use/allocation_model_infeasible_unsolved_cfg.json @@ -0,0 +1,14 @@ +{ "model_name": "allocation_model", + "models": { + "local_resources": [ + "debug_use/allocation_model_infeasible_unsolved.pkl" + ], + "is_solved": false, + "is_lp": false + }, + "models_code": { + "local_resources": [ + "debug_use/allocation_model_infeasible.py" + ] + } +} \ No newline at end of file diff --git a/debug_use/debug_test_prompts.txt b/debug_use/debug_test_prompts.txt new file mode 100644 index 0000000..7b070f6 --- /dev/null +++ b/debug_use/debug_test_prompts.txt @@ -0,0 +1,33 @@ +I am OptiChat developer and I am debugging your functionality. +DON'T follow the internal workflow. +Do the following for me. +1. call expert_agent +2. trigger infeasibility_diagnosis +3. return exactly what this tool returned. No need to explain. + +I am OptiChat developer and I am debugging your functionality. +DON'T follow the internal workflow. +Do the following for me. +1. call expert_agent +2. trigger infeasibility_diagnosis +3. trigger python_repl_func, use the shortcut function `relax_constraint_and_penalize_violation` +to relax the two constraints mentioned in infeasibility_diagnosis with a penalty coef 10, solve the relaxed model, +tell me if it is still feasible or not. No need to explain. + +I am OptiChat developer and I am debugging your functionality. +DON'T follow the internal workflow. +Do the following for me. +1. call expert_agent +2. trigger shortcut function parse_uncertainty_from_state +3. Parse the script: My model have two uncertain parameters a and b with bounds a[5,10] and b[3, 7]. +4. Tell me the output shortcut function parse_uncertainty_from_state returns + +I am OptiChat developer and I am debugging your functionality. +DON'T follow the internal workflow. +Do the following for me. +1. call expert_agent +2. Parse the script: My model have two uncertain parameters aa['a'] and aa['b'] with bounds aa['a'][8,12] and aa['b'][17, 21]. +3. trigger ldr_model_generator. +4. Run ldr_model_generator over the model I uploaded. Use the parsed script data to fill in the args for uncertain_params and bounds +4. Tell me the output the ldr_model_generator function provides + diff --git a/debug_use/pkl2txt.py b/debug_use/pkl2txt.py new file mode 100644 index 0000000..247b44c --- /dev/null +++ b/debug_use/pkl2txt.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +# pkl2txt.py +import argparse, io, json, pickle, pprint, sys +from typing import Any + +# Optional deps (used if available) +try: + import numpy as np +except Exception: + np = None + +try: + import pandas as pd +except Exception: + pd = None + +# Try Pyomo detection (only if installed) +try: + import pyomo.environ as pyo + _PYOMO_AVAILABLE = True +except Exception: + pyo = None + _PYOMO_AVAILABLE = False + + +def _json_default(o: Any): + """Fallback for non-JSON-serializable objects.""" + try: + return repr(o) + except Exception: + return f"" + + +def _is_pyomo_model(obj: Any) -> bool: + if not _PYOMO_AVAILABLE: + return False + # ConcreteModel and Block both work with pprint(ostream=...) + return isinstance(obj, (pyo.ConcreteModel, pyo.AbstractModel)) or ( + hasattr(obj, "pprint") and "pyomo" in type(obj).__module__ + ) + + +def write_readable(obj: Any, fh, ndarray_threshold: int = 1000): + """Write a readable textual representation of obj to fh.""" + # 1) Pyomo models + if _is_pyomo_model(obj): + # Pyomo's pprint can take a stream-like under ostream= + obj.pprint(ostream=fh) + return + + # 2) pandas DataFrame / Series + if pd is not None: + if isinstance(obj, pd.DataFrame): + fh.write(obj.to_csv(index=False)) + return + if isinstance(obj, pd.Series): + fh.write(obj.to_string()) + return + + # 3) numpy arrays + if np is not None and isinstance(obj, np.ndarray): + s = np.array2string(obj, threshold=ndarray_threshold, edgeitems=10) + fh.write(s + "\n") + return + + # 4) Simple Python containers / scalars → JSON if possible, else pprint + simple_types = (dict, list, tuple, set, str, int, float, bool, type(None)) + if isinstance(obj, simple_types): + try: + fh.write(json.dumps(obj, indent=2, default=_json_default)) + fh.write("\n") + except TypeError: + fh.write(pprint.pformat(obj, compact=False, width=100)) + fh.write("\n") + return + + # 5) Bytes → try utf-8, else hex + if isinstance(obj, (bytes, bytearray)): + try: + fh.write(bytes(obj).decode("utf-8")) + except Exception: + fh.write(bytes(obj).hex()) + fh.write("\n") + return + + # 6) Fallback + fh.write(repr(obj) + "\n") + + +def main(): + ap = argparse.ArgumentParser(description="Convert a .pkl file to a readable .txt file.") + ap.add_argument("input", help="Path to input .pkl") + ap.add_argument("-o", "--output", help="Path to output .txt (default: input with .txt)") + ap.add_argument("--ndarray-threshold", type=int, default=1000, + help="Max elements to show before summarizing numpy arrays (default: 1000)") + args = ap.parse_args() + + out_path = args.output or (args.input.rsplit(".", 1)[0] + ".txt") + + # WARNING: only load trusted pickles! + with open(args.input, "rb") as f: + obj = pickle.load(f) + + # If the pickle contains multiple objects (e.g., a tuple/list), we handle that gracefully + with open(out_path, "w", encoding="utf-8") as out: + if isinstance(obj, (list, tuple)) and len(obj) > 0: + out.write(f"# Pickle contained a {type(obj).__name__} of length {len(obj)}\n\n") + for i, item in enumerate(obj): + out.write(f"## Item {i} — {type(item).__name__}\n") + write_readable(item, out, ndarray_threshold=args.ndarray_threshold) + out.write("\n") + else: + write_readable(obj, out, ndarray_threshold=args.ndarray_threshold) + + print(f"Wrote readable text to: {out_path}") + + +if __name__ == "__main__": + main() diff --git a/debug_use/supply_chain_feasible.py b/debug_use/supply_chain_feasible.py new file mode 100644 index 0000000..6d8a1e4 --- /dev/null +++ b/debug_use/supply_chain_feasible.py @@ -0,0 +1,189 @@ +import numpy as np +import pyomo.environ as pyo +# import cloudpickle as pickle + + +time_periods = 5 +seed = None +data = { +'retailers_idx' :[0, 1], # Index of retailers +'distributors_idx' :[2, 3, 4], # Index of distributors +'producers_idx': [5, 6], # Index of producers +'raw_distributors_idx': [7, 8], # Index of raw distributors +'unit_price' :{0:5, 1:4}, # unit sales price at stages [0, 1] +'holding_cost' : {0:0.5, 1:0.4, 2:0.2, 3:0.3, 4:0.2, 5:0.3, 6:0.3}, # holding cost at stages [0, 1, 2, 3] +'supply_capacity' : {5: 40, 6: 40, 7: 50, 8: 40}, # production capacity at stages [1, 2, 3] +'reordering_routes': [(2,0), (2,1), (3,0), (3,1), (4,1), (5,2), (5,3), (5,4), (6,2), (6,4), (7,5), (7,6), (8,5), (8,6)], # reordering routes +'lead_time' : {(2,0): 1, (2,1):1, (3,0):1, (3,1):1, (4,1):1, (5,2):1, (5,3):1, (5,4):1, (6,2):1, (6,4):1, (7,5):1, (7,6):1, (8,5):1, (8,6):1}, # lead times at stages [0, 1, 2] +'demand_cost' : {0:1.5, 1:1}, # unit backlog cost at stages [0, 1, 2, 3] +'unit_cost' : {0:0.5, 1:0.3, 2:0.8, 3:0.7, 4:0.8, 5:0.1, 6:0.7, 7:0.5, 8:0.3, 9:0.4, 10:0.1, 11:0.7, 12:0.1, 13:0.2}, +'discount' : 0.97, # discount factor +'init_inv' :{0:20, 1:15, 2:30, 3:40, 4:30, 5:20, 6:30}, # Initial inventory +'demand_mean': {0:10, 1:15}, # Demand mean +'discount': 0.97, # Time value for money (Discount factor) +'uncertainty': 0.1, +'num_main_nodes':7, # Total number of main nodes (Retailer + Distributor + Producer) +'jin':{0: [2, 3], 1: [2, 3, 4], 2: [5, 6], 3: [5], 4: [5, 6], 5: [7, 8], 6: [7, 8]}, +'jout':{2: [0, 1], 3: [0, 1], 4: [1], 5: [2, 3, 4], 6: [2, 4], 7: [5, 6], 8:[5, 6]}, +'reorder_mapping' : {(2,0): 0, (2,1):1, (3,0):2, (3,1):3, (4,1):4, (5,2):5, (5,3):6, (5,4):7, (6,2):8, (6,4):9, (7,5):10, (7,6):11, (8,5):12, (8,6):13}, # lead times at stages [0, 1, 2] +'reverse_reorder_mapping': { + 0: (2, 0), + 1: (2, 1), + 2: (3, 0), + 3: (3, 1), + 4: (4, 1), + 5: (5, 2), + 6: (5, 3), + 7: (5, 4), + 8: (6, 2), + 9: (6, 4), + 10: (7, 5), + 11: (7, 6), + 12: (8, 5), + 13: (8, 6) +}} + + +if seed is None: + rng = np.random.default_rng() +else: + rng = np.random.default_rng(seed=42) + +model = pyo.ConcreteModel() + +# ------------------- sets ------------------------------------------------- +model.T0 = pyo.RangeSet(0, time_periods) +model.T = pyo.RangeSet(1, time_periods) +model.Tp = pyo.RangeSet(1, time_periods + 1) + +model.R = pyo.Set(initialize=data["retailers_idx"]) +model.D = pyo.Set(initialize=data["distributors_idx"]) +model.P = pyo.Set(initialize=data["producers_idx"]) +model.S = pyo.Set(initialize=data["raw_distributors_idx"]) + +model.MAIN = model.R | model.D | model.P +model.MAIN2 = model.P | model.S +model.ROUTES = pyo.Set(initialize=data["reordering_routes"]) + +model.JIN = pyo.Set(data["jin"].keys(), within=pyo.Any, initialize=data["jin"]) +model.JOUT = pyo.Set(data["jout"].keys(), within=pyo.Any, initialize=data["jout"]) + +# ------------------- static parameters ------------------------------------ +model.unit_price = pyo.Param(model.MAIN, initialize=data["unit_price"]) +model.h_cost = pyo.Param(model.MAIN, initialize=data["holding_cost"]) +model.cap = pyo.Param(model.MAIN2, initialize=data["supply_capacity"]) +model.lt = pyo.Param(model.ROUTES, initialize=data["lead_time"]) +model.b_cost = pyo.Param(model.R, initialize=data["demand_cost"]) + +# remap unit_cost keys from (j,k)→idx if needed +updated_unit_cost_dict = {data['reverse_reorder_mapping'].get(old_key, old_key): value for old_key, value in data['unit_cost'].items()} +model.u_cost = pyo.Param(model.ROUTES, initialize=updated_unit_cost_dict) + +model.init_inv = pyo.Param(model.MAIN, mutable = True, initialize=data["init_inv"]) + +# ------------------- *mutable* demand parameters -------------------------- +demand_dict = { + (t, r): float(rng.integers(low=0, high=10)) # random demand 8…19 + for t in model.T # (1 … time_periods) + for r in model.R +} +model.demand = pyo.Param(model.T, model.R, + mutable=True, + initialize=demand_dict) + +# ------------------- decision variables ----------------------------------- +model.I = pyo.Var(model.Tp, model.MAIN, domain=pyo.NonNegativeReals) +model.Tinv = pyo.Var(model.Tp, model.ROUTES, domain=pyo.NonNegativeReals) +model.Rqty = pyo.Var(model.T, model.ROUTES, domain=pyo.NonNegativeReals) +model.Sales= pyo.Var(model.T, model.R, domain=pyo.NonNegativeReals) +model.Back = pyo.Var(model.T, model.R, domain=pyo.NonNegativeReals) + +# ------------------- constraints ------------------------------------------ +def inv_balance(m, t, j): + if j in m.P | m.D: + if t == 0: + return m.I[t+1, j] == m.init_inv[j] + return ( + m.I[t+1, j] == + m.I[t, j] + + sum(m.Rqty[t - m.lt[k, j], k, j] + for k in m.JIN[j] if t - m.lt[k, j] >= 1) + - sum(m.Rqty[t, j, k] for k in m.JOUT[j]) + ) + else: + if t == 0: + return m.I[t+1, j] == m.init_inv[j] + return ( + m.I[t+1, j] == + m.I[t, j] + + sum(m.Rqty[t - m.lt[k, j], k, j] + for k in m.JIN[j] if t - m.lt[k, j] >= 1) + - m.Sales[t, j] + ) +model.inv_bal = pyo.Constraint(model.T0, model.MAIN, rule=inv_balance) + +def pipe_balance(m, t, j, k): + if t == 0: + return m.Tinv[t+1, j, k] == 0 + if t - m.lt[j, k] >= 1: + return ( + m.Tinv[t+1, j, k] == + m.Tinv[t, j, k] - m.Rqty[t - m.lt[j, k], j, k] + m.Rqty[t, j, k] + ) + return ( + m.Tinv[t+1, j, k] == + m.Tinv[t, j, k] + m.Rqty[t, j, k] + ) +model.pipe_bal = pyo.Constraint(model.T0, model.ROUTES, rule=pipe_balance) + +def reorder_cap(m, t, j): + if j in m.MAIN2: + return sum(m.Rqty[t, j, k] for k in m.JOUT[j]) <= m.cap[j] + return pyo.Constraint.Skip +model.re_cap = pyo.Constraint(model.T, model.MAIN2, rule=reorder_cap) + +def reorder_inv(m, t, j): + if j in m.D | m.P: + return sum(m.Rqty[t, j, k] for k in m.JOUT[j]) <= m.I[t, j] + return pyo.Constraint.Skip +model.re_inv = pyo.Constraint(model.T, model.D | model.P, rule=reorder_inv) + +def sales_demand(m, t, r): + rhs = m.demand[t, r] + (m.Back[t-1, r] if t > 1 else 0) + return m.Sales[t, r] <= rhs +model.sales1 = pyo.Constraint(model.T, model.R, rule=sales_demand) + +def sales_stock(m, t, r): + avail = m.I[t, r] + sum(m.Rqty[t - m.lt[k, r], k, r] + for k in m.JIN[r] if t - m.lt[k, r] >= 1) + return m.Sales[t, r] <= avail +model.sales2 = pyo.Constraint(model.T, model.R, rule=sales_stock) + +def backlog_def(m, t, r): + if t == 1: + return m.Back[t, r] == m.demand[t, r] - m.Sales[t, r] + return m.Back[t, r] == m.demand[t, r] + m.Back[t-1, r] - m.Sales[t, r] +model.backlog = pyo.Constraint(model.T, model.R, rule=backlog_def) + +# # end-horizon inventory target +model.final_inv = pyo.Constraint( + model.MAIN, rule=lambda m, j: m.I[time_periods + 1, j] == m.init_inv[j] +) + +# ------------------- objective ------------------------------------------- +revenue = sum(model.unit_price[r] * model.Sales[t, r] for t in model.T for r in model.R) +cost_re = sum(model.u_cost[route] * model.Rqty[t, route] for t in model.T for route in model.ROUTES) +cost_bk = sum(model.b_cost[r] * model.Back[t, r] for t in model.T for r in model.R) +cost_h = sum(model.h_cost[j] * model.I[t+1, j] for t in model.T for j in model.MAIN) + +model.obj = pyo.Objective(expr = revenue - cost_re - cost_bk - cost_h, + sense = pyo.maximize) + + + +# with open("supply_chain_model.pkl", "wb") as f: +# pickle.dump(model, f) + + + + diff --git a/debug_use/supply_chain_model.json b/debug_use/supply_chain_model.json new file mode 100644 index 0000000..a08c697 --- /dev/null +++ b/debug_use/supply_chain_model.json @@ -0,0 +1,14 @@ +{ "model_name": "allocation_model", + "models": { + "local_resources": [ + "debug_use/supply_chain_model.pkl" + ], + "is_solved": false, + "is_lp": false + }, + "models_code": { + "local_resources": [ + "debug_use/supply_chain_feasible.py" + ] + } +} \ No newline at end of file diff --git a/debug_use/supply_chain_model.pkl b/debug_use/supply_chain_model.pkl new file mode 100644 index 0000000..3bea370 Binary files /dev/null and b/debug_use/supply_chain_model.pkl differ diff --git a/my_model.pkl b/my_model.pkl new file mode 100644 index 0000000..959aeb5 Binary files /dev/null and b/my_model.pkl differ diff --git a/optichat/.env b/optichat/.env new file mode 100644 index 0000000..e69de29 diff --git a/optichat/__init__.py b/optichat/__init__.py new file mode 100644 index 0000000..63bd45e --- /dev/null +++ b/optichat/__init__.py @@ -0,0 +1 @@ +from . import agent \ No newline at end of file diff --git a/optichat/agent.py b/optichat/agent.py new file mode 100644 index 0000000..f0aba39 --- /dev/null +++ b/optichat/agent.py @@ -0,0 +1,11 @@ +import os +import logging + +# Disable OpenTelemetry to avoid context management issues +os.environ["OTEL_SDK_DISABLED"] = "true" +# Suppress OpenTelemetry warnings +logging.getLogger("opentelemetry").setLevel(logging.ERROR) + +from optichat.sub_agents.root.agent import create_root_agent + +root_agent = create_root_agent(workflow="default") \ No newline at end of file diff --git a/optichat/config/cfg_template.py b/optichat/config/cfg_template.py new file mode 100644 index 0000000..5e4691e --- /dev/null +++ b/optichat/config/cfg_template.py @@ -0,0 +1,28 @@ +import json + +cfg_template = { + "model_name": "your_model_name_here", + "models": { + "local_resources": [ + "models/*", + "models/**/*.pkl" + ] + }, + "models_code": { + "local_resources": [ + "models_code/*", + "models_code/**/*.py" + ] + }, + "models_paper": { + "local_resources": [ + "models_paper/*", + "models_paper/**/*.txt", + "models_paper/**/*.pdf" + ] + } +} + +# json dump example +with open("cfg_template.json", "w") as f: + json.dump(cfg_template, f, indent=4) diff --git a/optichat/config/constants.py b/optichat/config/constants.py new file mode 100644 index 0000000..25827d1 --- /dev/null +++ b/optichat/config/constants.py @@ -0,0 +1,62 @@ + +IS_SESSION_INITIALIZED = "IS_SESSION_INITIALIZED" + +USER_QUERY = "USER_QUERY" + +OUTPUT_KEY_ROOT_AGENT = "OUTPUT_KEY_ROOT_AGENT" + +IS_EXPERT_AGENT_USED = "IS_EXPERT_AGENT_USED" +EXPERT_AGENT_PYTHON_REPL_FUNC_USES = "EXPERT_AGENT_PYTHON_REPL_FUNC_USES" +EXPERT_AGENT_PYTHON_REPL_FUNC_MAX_USES = "EXPERT_AGENT_PYTHON_REPL_FUNC_MAX_USES" +EXPERT_AGENT_PYTHON_REPL_FUNC_MAX_TOKENS = "EXPERT_AGENT_PYTHON_REPL_FUNC_MAX_TOKENS" +EXPERT_AGENT_CODE_RAG_USES = "EXPERT_AGENT_CODE_RAG_USES" +EXPERT_AGENT_CODE_RAG_MAX_USES = "EXPERT_AGENT_CODE_RAG_MAX_USES" +EXPERT_AGENT_CODE_BLOCK = "EXPERT_AGENT_CODE_BLOCK" +EXPERT_AGENT_PAPER_RAG_USES = "EXPERT_AGENT_PAPER_RAG_USES" +EXPERT_AGENT_PAPER_RAG_MAX_USES = "EXPERT_AGENT_PAPER_RAG_MAX_USES" +EXPERT_AGENT_PAPER_CONTENT = "EXPERT_AGENT_PAPER_CONTENT" +EXPERT_AGENT_START_TIME = "EXPERT_AGENT_START_TIME" +OUTPUT_KEY_EXPERT_AGENT = "OUTPUT_KEY_EXPERT_AGENT" + +MODELS_DICTIONARY = "MODELS_DICTIONARY" +MODEL_VERSIONS = "MODEL_VERSIONS" +IS_MODELS_DICTIONARY_AVAILABLE = "IS_MODELS_DICTIONARY_AVAILABLE" +IS_MODELS_CODE_AVAILABLE = "IS_MODELS_CODE_AVAILABLE" +IS_MODELS_PAPER_AVAILABLE = "IS_MODELS_PAPER_AVAILABLE" + +CFG = "CFG" + +TMP_ROOT_FOLDER = "tmp" +TMP_MODEL_OBJECT_FOLDER = "tmp/model_objects" + + +# NOT reset within a session +PERSISTENT_STATES = { + IS_SESSION_INITIALIZED: False, + OUTPUT_KEY_ROOT_AGENT: "", + EXPERT_AGENT_PYTHON_REPL_FUNC_MAX_USES: 5, + EXPERT_AGENT_PYTHON_REPL_FUNC_MAX_TOKENS: 10000, + EXPERT_AGENT_CODE_RAG_MAX_USES: 1, + EXPERT_AGENT_CODE_BLOCK: "", + EXPERT_AGENT_PAPER_RAG_MAX_USES: 1, + EXPERT_AGENT_PAPER_CONTENT: "", + OUTPUT_KEY_EXPERT_AGENT: "", + MODELS_DICTIONARY: {}, + MODEL_VERSIONS: [], + IS_MODELS_DICTIONARY_AVAILABLE: False, + IS_MODELS_CODE_AVAILABLE: False, + IS_MODELS_PAPER_AVAILABLE: False, + CFG: {}, +} + + +# reset for every query +TEMPORARY_STATES = { + USER_QUERY: "", + IS_EXPERT_AGENT_USED: False, + EXPERT_AGENT_PYTHON_REPL_FUNC_USES: 3, + EXPERT_AGENT_CODE_RAG_USES: 1, + EXPERT_AGENT_PAPER_RAG_USES: 1, + EXPERT_AGENT_START_TIME: False, +} + diff --git a/optichat/config/llm_cfg.py b/optichat/config/llm_cfg.py new file mode 100644 index 0000000..1c88ce7 --- /dev/null +++ b/optichat/config/llm_cfg.py @@ -0,0 +1,12 @@ +# LLM MODELS +GPT_5 = "openai/gpt-5" +GPT_5_TEMPERATURE = 1 # gpt-5 models don't support temperature=0. Only temperature=1 is supported +GPT_5_MAX_TOKENS = 30000 + +GPT_5_MINI = "openai/gpt-5-mini" +GPT_5_MINI_TEMPERATURE = 1 +GPT_5_MINI_MAX_TOKENS = 30000 + +GPT_5_NANO = "openai/gpt-5-nano" +GPT_5_NANO_TEMPERATURE = 1 +GPT_5_NANO_MAX_TOKENS = 30000 diff --git a/optichat/config/pyomo_mapping.py b/optichat/config/pyomo_mapping.py new file mode 100644 index 0000000..2d24598 --- /dev/null +++ b/optichat/config/pyomo_mapping.py @@ -0,0 +1,4 @@ +from pyomo.opt import SolverStatus, TerminationCondition + +TerminationConditionMap = {TerminationCondition.infeasible, + TerminationCondition.infeasibleOrUnbounded} \ No newline at end of file diff --git a/optichat/config/rag_cfg.py b/optichat/config/rag_cfg.py new file mode 100644 index 0000000..2f126de --- /dev/null +++ b/optichat/config/rag_cfg.py @@ -0,0 +1,49 @@ +EMBEDDING_MODEL = "text-embedding-3-large" +PERSIST_DIRECTORY = "./chroma_langchain_db" + + +# the minimum number of lines that the source code file must have to be segmented using the parser +CODE_RAG_PARSER_THRESHOLD = 100 +CODE_RAG_IS_SPLITTED = False +CODE_RAG_CHUNK_SIZE = 100 +CODE_RAG_CHUNK_OVERLAP = 10 +CODE_RAG_SEARCH_TYPE = "similarity" # similarity”, “mmr”, or “similarity_score_threshold” +CODE_RAG_NUM_OF_RESULTS = 4 +CODE_RAG_NUM_OF_FETCH_K = 10 # only for mmr +CODE_RAG_LAMBDA_MULT = 0.5 # only for mmr +CODE_RAG_SCORE_THRESHOLD = 0.8 # only for similarity_score_threshold +if CODE_RAG_SEARCH_TYPE == "similarity": + CODE_RAG_SEARCH_KWARGS = {"k": CODE_RAG_NUM_OF_RESULTS} +elif CODE_RAG_SEARCH_TYPE == "mmr": + CODE_RAG_SEARCH_KWARGS = {"k": CODE_RAG_NUM_OF_RESULTS, + "fetch_k": CODE_RAG_NUM_OF_FETCH_K, + "lambda_mult": CODE_RAG_LAMBDA_MULT} +elif CODE_RAG_SEARCH_TYPE == "similarity_score_threshold": + CODE_RAG_SEARCH_KWARGS = {"k": CODE_RAG_NUM_OF_RESULTS, + "score_threshold": CODE_RAG_SCORE_THRESHOLD} +else: + raise ValueError(f"CODE_RAG_SEARCH_TYPE {CODE_RAG_SEARCH_TYPE} not supported.") + + +PAPER_RAG_IS_SPLITTED = False +PAPER_RAG_CHUNK_SIZE = 1000 +PAPER_RAG_CHUNK_OVERLAP = 100 +PAPER_RAG_SEARCH_TYPE = "similarity" # similarity”, “mmr”, or “similarity_score_threshold” +PAPER_RAG_NUM_OF_RESULTS = 4 +PAPER_RAG_NUM_OF_FETCH_K = 10 # only for mmr +PAPER_RAG_LAMBDA_MULT = 0.5 # only for mmr +PAPER_RAG_SCORE_THRESHOLD = 0.8 # only for similarity_score_threshold +if PAPER_RAG_SEARCH_TYPE == "similarity": + PAPER_RAG_SEARCH_KWARGS = {"k": PAPER_RAG_NUM_OF_RESULTS} +elif PAPER_RAG_SEARCH_TYPE == "mmr": + PAPER_RAG_SEARCH_KWARGS = {"k": PAPER_RAG_NUM_OF_RESULTS, + "fetch_k": PAPER_RAG_NUM_OF_FETCH_K, + "lambda_mult": PAPER_RAG_LAMBDA_MULT} +elif PAPER_RAG_SEARCH_TYPE == "similarity_score_threshold": + PAPER_RAG_SEARCH_KWARGS = {"k": PAPER_RAG_NUM_OF_RESULTS, + "score_threshold": PAPER_RAG_SCORE_THRESHOLD} +else: + raise ValueError(f"PAPER_RAG_SEARCH_TYPE {PAPER_RAG_SEARCH_TYPE} not supported.") + + + diff --git a/optichat/expl_recipes/expl_recipes.py b/optichat/expl_recipes/expl_recipes.py new file mode 100644 index 0000000..f5f103e --- /dev/null +++ b/optichat/expl_recipes/expl_recipes.py @@ -0,0 +1,16 @@ +EXPL_RECIPES = """ +**FACTUAL EXPLANATION: TRADE-OFFS** +- Users may be curious about the rationale behind certain decisions made by the model. +This is caused by the trade-offs that the model balances competing objectives while satisfying all constraints. +- To locate the root cause, focus on what decision variables are penalized in the objective function, and by how much, +and how these decision variables interact with other decision variables through constraints. + +**COUNTERFACTUAL EXPLANATION: NEW SCENARIO** +- Users may be suspicious of certain decisions or interested in exploring alternatives. +This represents a new scenario that requires the original model to be modified and re-solved for comparison. +- Since resolving new models is costly, this explanation strategy should only be used +when the user explicitly requests it in the query and indicates a clear modification object and extent. + e.g. "What will happen if the penalty of overage is doubled?" + e.g. "What if we need the generator A and B to produce at least X MW in total every hour?" + e.g. "Why can't we force the station to have Y routes instead of Z routes every day?" +""" \ No newline at end of file diff --git a/optichat/llm.py b/optichat/llm.py new file mode 100644 index 0000000..b4d386c --- /dev/null +++ b/optichat/llm.py @@ -0,0 +1,13 @@ +from google.adk.models.lite_llm import LiteLlm +from optichat.config.llm_cfg import * + +gpt_5 = LiteLlm(model=GPT_5, + temperature=GPT_5_TEMPERATURE, + max_tokens=GPT_5_MAX_TOKENS) +gpt_5_mini = LiteLlm(model=GPT_5_MINI, + temperature=GPT_5_MINI_TEMPERATURE, + max_tokens=GPT_5_MINI_MAX_TOKENS) +gpt_5_nano = LiteLlm(model=GPT_5_NANO, + temperature=GPT_5_NANO_TEMPERATURE, + max_tokens=GPT_5_NANO_MAX_TOKENS) + diff --git a/optichat/new_requirements.txt b/optichat/new_requirements.txt new file mode 100644 index 0000000..d38d49c --- /dev/null +++ b/optichat/new_requirements.txt @@ -0,0 +1,235 @@ +absolufy-imports==0.3.1 +aiohappyeyeballs==2.6.1 +aiohttp==3.12.14 +aiosignal==1.4.0 +alembic==1.16.5 +altair==5.5.0 +annotated-types==0.7.0 +anyio==4.9.0 +appnope @ file:///home/conda/feedstock_root/build_artifacts/appnope_1733332318622/work +asttokens @ file:///home/conda/feedstock_root/build_artifacts/asttokens_1733250440834/work +async-timeout==4.0.3 +attrs==25.3.0 +Authlib==1.6.3 +backoff==2.2.1 +bcrypt==4.3.0 +blinker==1.9.0 +build==1.3.0 +cachetools==5.5.2 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +chromadb==1.1.0 +click==8.1.8 +cloudpickle==3.1.1 +coloredlogs==15.0.1 +comm @ file:///home/conda/feedstock_root/build_artifacts/comm_1733502965406/work +contourpy==1.3.1 +cryptography==45.0.7 +cycler==0.12.1 +dataclasses-json==0.6.7 +datasets==4.0.0 +debugpy @ file:///Users/runner/miniforge3/conda-bld/debugpy_1744321312502/work +decorator @ file:///home/conda/feedstock_root/build_artifacts/decorator_1740384970518/work +Deprecated==1.2.18 +dill==0.3.8 +distro==1.9.0 +docstring_parser==0.17.0 +durationpy==0.10 +exceptiongroup==1.2.2 +executing @ file:///home/conda/feedstock_root/build_artifacts/executing_1745502089858/work +fastapi==0.116.1 +fastuuid==0.12.0 +filelock==3.18.0 +flatbuffers==25.2.10 +fonttools==4.56.0 +frozenlist==1.7.0 +fsspec==2025.3.0 +gitdb==4.0.12 +GitPython==3.1.44 +google-adk==1.13.0 +google-api-core==2.25.1 +google-api-python-client==2.181.0 +google-auth==2.40.3 +google-auth-httplib2==0.2.0 +google-cloud-aiplatform==1.111.0 +google-cloud-appengine-logging==1.6.2 +google-cloud-audit-log==0.3.2 +google-cloud-bigquery==3.36.0 +google-cloud-bigtable==2.32.0 +google-cloud-core==2.4.3 +google-cloud-logging==3.12.1 +google-cloud-resource-manager==1.14.2 +google-cloud-secret-manager==2.24.0 +google-cloud-spanner==3.57.0 +google-cloud-speech==2.33.0 +google-cloud-storage==2.19.0 +google-cloud-trace==1.16.2 +google-crc32c==1.7.1 +google-genai==1.33.0 +google-resumable-media==2.7.2 +googleapis-common-protos==1.70.0 +graphviz==0.21 +greenlet==3.2.4 +grpc-google-iam-v1==0.14.2 +grpc-interceptor==0.15.4 +grpcio==1.74.0 +grpcio-status==1.74.0 +gurobipy==12.0.1 +h11==0.14.0 +hf-xet==1.1.5 +httpcore==1.0.7 +httplib2==0.30.0 +httptools==0.6.4 +httpx==0.28.1 +httpx-sse==0.4.1 +huggingface-hub==0.33.4 +humanfriendly==10.0 +idna==3.10 +importlib_metadata==8.4.0 +importlib_resources==6.5.2 +iniconfig==2.1.0 +ipykernel @ file:///Users/runner/miniforge3/conda-bld/ipykernel_1719845458456/work +ipython @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_ipython_1748711175/work +jedi @ file:///home/conda/feedstock_root/build_artifacts/jedi_1733300866624/work +Jinja2==3.1.6 +jiter==0.9.0 +jsonpatch==1.33 +jsonpointer==3.0.0 +jsonschema==4.23.0 +jsonschema-specifications==2024.10.1 +jupyter_client @ file:///home/conda/feedstock_root/build_artifacts/jupyter_client_1733440914442/work +jupyter_core @ file:///home/conda/feedstock_root/build_artifacts/jupyter_core_1748333051527/work +kiwisolver==1.4.8 +kubernetes==33.1.0 +langchain==0.3.27 +langchain-chroma==0.2.6 +langchain-community==0.3.29 +langchain-core==0.3.76 +langchain-experimental==0.3.4 +langchain-openai==0.3.33 +langchain-text-splitters==0.3.11 +langsmith==0.4.23 +litellm==1.76.2 +loguru==0.7.3 +Mako==1.3.10 +markdown-it-py==4.0.0 +MarkupSafe==3.0.2 +marshmallow==3.26.1 +matplotlib==3.10.1 +matplotlib-inline @ file:///home/conda/feedstock_root/build_artifacts/matplotlib-inline_1733416936468/work +mcp==1.13.1 +mdurl==0.1.2 +mmh3==5.2.0 +mpmath==1.3.0 +multidict==6.6.3 +multiprocess==0.70.16 +mypy_extensions==1.1.0 +narwhals==1.31.0 +nest_asyncio @ file:///home/conda/feedstock_root/build_artifacts/nest-asyncio_1733325553580/work +networkx==3.4.2 +numpy==2.2.4 +oauthlib==3.3.1 +onnxruntime==1.22.1 +openai==1.106.0 +opentelemetry-api==1.37.0 +opentelemetry-exporter-gcp-trace==1.9.0 +opentelemetry-exporter-otlp-proto-common==1.37.0 +opentelemetry-exporter-otlp-proto-grpc==1.37.0 +opentelemetry-proto==1.37.0 +opentelemetry-resourcedetector-gcp==1.9.0a0 +opentelemetry-sdk==1.37.0 +opentelemetry-semantic-conventions==0.58b0 +orjson==3.11.3 +overrides==7.7.0 +packaging==24.2 +pandas==2.2.3 +parso @ file:///home/conda/feedstock_root/build_artifacts/parso_1733271261340/work +pexpect @ file:///home/conda/feedstock_root/build_artifacts/pexpect_1733301927746/work +pickleshare @ file:///home/conda/feedstock_root/build_artifacts/pickleshare_1733327343728/work +pillow==11.1.0 +platformdirs @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_platformdirs_1746710438/work +pluggy==1.5.0 +ply==3.11 +posthog==5.4.0 +prompt_toolkit @ file:///home/conda/feedstock_root/build_artifacts/prompt-toolkit_1744724089886/work +propcache==0.3.2 +proto-plus==1.26.1 +protobuf==6.32.0 +psutil @ file:///Users/runner/miniforge3/conda-bld/psutil_1740663154588/work +ptyprocess @ file:///home/conda/feedstock_root/build_artifacts/ptyprocess_1733302279685/work/dist/ptyprocess-0.7.0-py2.py3-none-any.whl#sha256=92c32ff62b5fd8cf325bec5ab90d7be3d2a8ca8c8a3813ff487a8d2002630d1f +pure_eval @ file:///home/conda/feedstock_root/build_artifacts/pure_eval_1733569405015/work +pyarrow==19.0.1 +pyasn1==0.6.1 +pyasn1_modules==0.4.2 +pybase64==1.4.2 +pycparser==2.22 +pydantic==2.11.7 +pydantic-settings==2.10.1 +pydantic_core==2.33.2 +pydeck==0.9.1 +Pygments @ file:///home/conda/feedstock_root/build_artifacts/pygments_1750615794071/work +pyomo==6.9.1 +pyparsing==3.2.1 +PyPika==0.48.9 +pyproject_hooks==1.2.0 +PySide6==6.8.2.1 +PySide6_Addons==6.8.2.1 +PySide6_Essentials==6.8.2.1 +pytest==8.3.3 +python-dateutil @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_python-dateutil_1751104122/work +python-dotenv==1.0.1 +python-multipart==0.0.20 +pytz==2025.1 +PyYAML==6.0.2 +pyzmq @ file:///Users/runner/miniforge3/conda-bld/pyzmq_1666828711931/work +referencing==0.36.2 +regex==2024.11.6 +requests==2.32.5 +requests-oauthlib==2.0.0 +requests-toolbelt==1.0.0 +rich==14.1.0 +rpds-py==0.23.1 +rsa==4.9.1 +shapely==2.1.1 +shellingham==1.5.4 +shiboken6==6.8.2.1 +six @ file:///home/conda/feedstock_root/build_artifacts/six_1733380938961/work +smmap==5.0.2 +sniffio==1.3.1 +SQLAlchemy==2.0.43 +sqlalchemy-spanner==1.16.0 +sqlparse==0.5.3 +sse-starlette==3.0.2 +stack_data @ file:///home/conda/feedstock_root/build_artifacts/stack_data_1733569443808/work +starlette==0.47.3 +streamlit==1.43.2 +sympy==1.14.0 +tenacity==8.5.0 +tiktoken==0.9.0 +tokenizers==0.22.0 +toml==0.10.2 +tomli==2.2.1 +tornado==6.4.2 +tqdm==4.67.1 +traitlets @ file:///home/conda/feedstock_root/build_artifacts/traitlets_1733367359838/work +typer==0.17.4 +typing-inspect==0.9.0 +typing-inspection==0.4.1 +typing_extensions==4.12.2 +tzdata==2025.1 +tzlocal==5.3.1 +uritemplate==4.2.0 +urllib3==2.3.0 +uvicorn==0.35.0 +uvloop==0.21.0 +watchdog==6.0.0 +watchfiles==1.1.0 +wcwidth @ file:///home/conda/feedstock_root/build_artifacts/wcwidth_1733231326287/work +websocket-client==1.8.0 +websockets==15.0.1 +wrapt==1.17.3 +xxhash==3.5.0 +yarl==1.20.1 +zipp @ file:///home/conda/feedstock_root/build_artifacts/zipp_1749421620841/work +zstandard==0.24.0 diff --git a/optichat/sub_agents/expert/agent.py b/optichat/sub_agents/expert/agent.py new file mode 100644 index 0000000..9ad7f60 --- /dev/null +++ b/optichat/sub_agents/expert/agent.py @@ -0,0 +1,42 @@ +from google.adk.agents import LlmAgent, BaseAgent, LoopAgent, SequentialAgent, ParallelAgent, Agent +# from google.adk.tools.agent_tool import AgentTool +# from google.adk.events import Event +from optichat.llm import * +from optichat.config.constants import * +from optichat.sub_agents.expert.prompt import get_expert_agent_prompt +from optichat.tools.search_tool import get_model_components +from optichat.tools.python_repl import python_repl_func +from optichat.tools.rag_tool import code_rag, paper_rag +from optichat.tools.callback_tool import (check_is_expert_agent_used, check_expert_agent_runtime, + check_llm_request, check_llm_response, check_tool_usage, check_tool_response) +from optichat.tools.custom_tool import infeasibility_diagnosis, ldr_model_generator, ldr_expression_generator, robustness_analysis + +def create_expert_agent(prompt_version=1, tools_version=1): + expert_agent_prompt = get_expert_agent_prompt(prompt_version) + if tools_version == 1: + expert_agent_tools = [get_model_components, + python_repl_func, + code_rag, + paper_rag, + infeasibility_diagnosis, + ldr_model_generator, + ldr_expression_generator, + robustness_analysis] # TODO: infeasibility diagnosis, ldr_model_generator and ldr_expression_generator, robustness_analysis under testing + else: + raise NotImplementedError(f"Tools version '{tools_version}' is not implemented.") + + expert_agent = Agent(name="expert_agent", + model=gpt_5, # remember to change it back under debugging + tools=expert_agent_tools, + description=("Optimization & operations research expert that " + "interacts with , , "), + instruction=expert_agent_prompt, + output_key=OUTPUT_KEY_EXPERT_AGENT, + before_agent_callback=check_is_expert_agent_used, + after_agent_callback=check_expert_agent_runtime, + before_model_callback=check_llm_request, + after_model_callback=check_llm_response, + before_tool_callback=check_tool_usage, + after_tool_callback=check_tool_response + ) + return expert_agent diff --git a/optichat/sub_agents/expert/prompt.py b/optichat/sub_agents/expert/prompt.py new file mode 100644 index 0000000..68e231c --- /dev/null +++ b/optichat/sub_agents/expert/prompt.py @@ -0,0 +1,245 @@ +from optichat.tools.extract_tool import auto_extract_function_docs + + +EXPERT_AGENT_PROMPT_NO_SC = """ +USER QUERY +{USER_QUERY} + +RESPONSIBILITIES +You are an optimization & operations research expert that +use CONTEXT TOOLS to interact with RESOURCES following the WORKFLOW, +and answers the USER QUERY based on the interactions. + +IMPORTANT ADDITIONAL GUIDELINES +1. Do not use just symbols or equations in your explanations. The user you are talking to is not an optimization expert. Always provide clear, natural-language descriptions and intuitive reasoning alongside your analysis, so the user can fully understand what is happening and why. +2. After obtaining the slack values for a constraint during infeasibility analysis or relaxation, identify the most practical parameter that could be adjusted in the real world to remove the infeasibility. Add the slack value to that parameter and report back explicitly to the user, e.g., “Parameter X should change from ___ to ___ for the model to become feasible.” If multiple parameters appear in the relaxed constraint, choose the one that makes the most sense to adjust from a real-world operational standpoint. + +RESOURCES + (dynamic availability: {IS_MODELS_DICTIONARY_AVAILABLE}): + the optimization models labelled with version names, {MODEL_VERSIONS}. + + (dynamic availability: {IS_MODELS_CODE_AVAILABLE}): + code used to implement the optimization models. + + (dynamic availability: {IS_MODELS_PAPER_AVAILABLE}): + scientific papers associated with the optimization models. + +CONTEXT TOOLS + - `infeasibility_diagnosis` + resource access: + result type: deterministic and slow, diagnose infeasibility of existing + - `get_model_components` + resource access: + result type: deterministic and fast, retrieve information about model components in existing + - `python_repl_func` (Limited Uses Left: {EXPERT_AGENT_PYTHON_REPL_FUNC_USES}) + resource access: + result type: dynamic and slow (error-prone), load, modify and solve new ONLY + - `code_rag` (Limited Uses Left: {EXPERT_AGENT_CODE_RAG_USES}) + resource access: + result type: dynamic and slow, retrieve code blocks + - `paper_rag` (Limited Uses Left: {EXPERT_AGENT_PAPER_RAG_USES}) + resource access: + result type: dynamic and slow, retrieve code contents + +WORKFLOW +1. classify the USER QUERY and find the appropriate explanation strategy from PRIOR KNOWLEDGE. + +2. use CONTEXT TOOLS to interact with RESOURCES for information gathering. + +3. answer the USER QUERY. + +4. special handling when infeasibility is detected: + If the model uploaded by the user has `sol_status` ∈ [TerminationCondition.infeasible, TerminationCondition.infeasibleOrUnbounded], + the following infeasibility resolution workflow MUST be executed carefully and sequentially: + + (a) **Trigger infeasibility diagnosis** + - Call the `infeasibility_diagnosis` context tool on the infeasible model. + - Collect the list of constraints reported as contributors to infeasibility. + - Return these constraints to the user in your response. + + (b) **Relaxation attempt** + - Check if the function `relax_constraint_and_penalize_violation` is available in the environment. + - If available: + i. Apply `relax_constraint_and_penalize_violation` on the *first constraint* identified by `infeasibility_diagnosis` + with a penalty coefficient of **10**. + ii. Solve the newly relaxed model. Make sure the newly relaxed model is a part of model dictionary (DO NOT MAKE UP RANDOM MODEL NAMES), if not double check the version name you are using. + iii. Return to the user: + • the constraint that was relaxed + • the updated termination condition of the relaxed model. If the termination condition is in [TerminationCondition.infeasible, TerminationCondition.infeasibleOrUnbounded] follow the steps mentioned in the **Iterative restoration loop**. + If the updated termination condition is in [TerminationCondition.optimal, TerminationCondition.feasible] follow the steps mentioned in **Termination condition**. + • the new list of infeasibility-causing constraints (if any) + - If not available: + → Explicitly inform the user that `relax_constraint_and_penalize_violation` could not be found, + and infeasibility restoration cannot continue automatically. + + (c) **Iterative restoration loop** + - After solving the relaxed model: + i. If the new `sol_status` ∈ [TerminationCondition.infeasible, TerminationCondition.infeasibleOrUnbounded], + then repeat Steps (a) and (b): + • trigger `infeasibility_diagnosis` again on the relaxed model + • identify the new first constraint reported as infeasible + • relax that constraint using the same penalty coefficient (10) + • solve the model again + continue this loop **until the model becomes feasible**. + ii. After each iteration, return to the user: + • the set of constraints returned by each `infeasibility_diagnosis` call, and + • the constraint relaxed by `relax_constraint_and_penalize_violation`. + + (d) **Termination condition** + - Once the model reaches a feasible solution (TerminationCondition.optimal or TerminationCondition.feasible), + stop the loop. + - Report the final feasible status and summarize: + • the total number of relaxation steps performed + • all constraints relaxed in sequence + • the penalty coefficients used. + + (e) **Fallback** + - If infeasibility persists even after 5 relaxation attempts, + notify the user explicitly that the model remains infeasible, + summarize all relaxed constraints, and recommend further manual inspection. + +5. Throughout this process: + - NEVER attempt random or exploratory modifications. + - Use `get_model_components` for retrieving detailed constraint or variable information as needed. + - Use `python_repl_func` ONLY to re-solve or rebuild models when explicitly required by the workflow. + - Use `code_rag` and `paper_rag` ONLY at the end, if additional version-agnostic technical reference is required. + +PRIOR KNOWLEDGE +__MODELS_RECIPE_PLACEHOLDER__ +__EXPLANATIONS_RECIPE_PLACEHOLDER__ + +TOOL CONVENTIONS +`get_model_components` conventions + - searching by component_type provides complete information about a component type efficiently + through a single tool call, but may be truncated if too many components are in . + - searching by pattern provides more granular filtering to prevent truncation, + but requires much more tool calls if complete information about a component type is desired. + - if new was solved in previous `python_repl_func` call, + complete information about the new can be retrieved by `get_model_components`. + - Examples: + get_model_components(["v1", "v2"], "objective", "", tool_context) compares objective between v1 and v2 + get_model_components(["v1"], "variable", "", tool_context) gets all decision variables in v1 + get_model_components(["v1"], "constraint", "", tool_context) gets all constraints in v1 + get_model_components(["v1"], "", "ramp*", tool_context) gets ramp-related components in v1 when previous result was truncated + get_model_components(["v2"], "constraint", "transport*", tool_context) gets transport-related constraints in v2 when previous result was truncated +`python_repl_func` conventions + - ONLY used when necessary: + only when USER QUERY explicitly falls into the categories that requires new in PRIOR KNOWLEDGE + - Concise code snippet: + STOP the code snippet as soon as new are programmed to be solved. + NEVER look up information about new in the code snippet. Use `get_model_components` instead + - Shortcut functions: + models_dictionary is a internal object that stores all and has already been loaded for you. + use the following generic shortcut functions and models_dictionary to load and solve . + HOWEVER, NEVER interact with models_dictionary directly as it is for internal use only. + __SHORTCUT_FUNCTIONS_PLACEHOLDER__ +`code_rag` & `paper_rag` conventions + - ONLY used in the end: + only when have been thoroughly analyzed with PRIOR KNOWLEDGE, + the code blocks and paper contents are version-agnostic and can ONLY serve as supplementary information + prioritize using `get_model_components` and `python_repl_func` first + +RESPONSE STYLE +- coherent and information-grounded narrative +- NEVER be obsessed with calculating statistics and verifying user's observations +- focus on **explanations and analysis** to answer the USER QUERY +- NEVER do extra work. NEVER explore randomly. +""" + +EXPERT_AGENT_PROMPT = """ +USER QUERY +{USER_QUERY} + +RESPONSIBILITIES +You are an optimization & operations research expert that +use CONTEXT TOOLS to interact with RESOURCES following the WORKFLOW, +and answers the USER QUERY based on the interactions. + +RESOURCES + (dynamic availability: {IS_MODELS_DICTIONARY_AVAILABLE}): + the optimization models labelled with version names, {MODEL_VERSIONS}. + + (dynamic availability: {IS_MODELS_CODE_AVAILABLE}): + code used to implement the optimization models. + + (dynamic availability: {IS_MODELS_PAPER_AVAILABLE}): + scientific papers associated with the optimization models. + +CONTEXT TOOLS + - `get_model_components` + resource access: + result type: deterministic and fast, retrieve information about model components in existing + - `python_repl_func` (Limited Uses Left: {EXPERT_AGENT_PYTHON_REPL_FUNC_USES}) + resource access: + result type: dynamic and slow (error-prone), load, modify and solve new ONLY + - `code_rag` (Limited Uses Left: {EXPERT_AGENT_CODE_RAG_USES}) + resource access: + result type: dynamic and slow, retrieve code blocks + - `paper_rag` (Limited Uses Left: {EXPERT_AGENT_PAPER_RAG_USES}) + resource access: + result type: dynamic and slow, retrieve code contents + +WORKFLOW +1. classify the USER QUERY and find the appropriate explanation strategy from PRIOR KNOWLEDGE +2. use CONTEXT TOOLS to interact with RESOURCES for information gathering +3. answer the USER QUERY + +PRIOR KNOWLEDGE +__MODELS_RECIPE_PLACEHOLDER__ +__EXPLANATIONS_RECIPE_PLACEHOLDER__ + +TOOL CONVENTIONS +`get_model_components` conventions + - searching by component_type provides complete information about a component type efficiently + through a single tool call, but may be truncated if too many components are in . + - searching by pattern provides more granular filtering to prevent truncation, + but requires much more tool calls if complete information about a component type is desired. + - if new was solved in previous `python_repl_func` call, + complete information about the new can be retrieved by `get_model_components`. + - Examples: + get_model_components(["v1", "v2"], "objective", "", tool_context) compares objective between v1 and v2 + get_model_components(["v1"], "variable", "", tool_context) gets all decision variables in v1 + get_model_components(["v1"], "constraint", "", tool_context) gets all constraints in v1 + get_model_components(["v1"], "", "ramp*", tool_context) gets ramp-related components in v1 when previous result was truncated + get_model_components(["v2"], "constraint", "transport*", tool_context) gets transport-related constraints in v2 when previous result was truncated +`python_repl_func` conventions + - ONLY used when necessary: + only when USER QUERY explicitly falls into the categories that requires new in PRIOR KNOWLEDGE + - Concise code snippet: + STOP the code snippet as soon as new are programmed to be solved. + NEVER look up information about new in the code snippet. Use `get_model_components` instead + - Generic code snippet: + the code snippet MUST be generic to built by different modelling languages, + NEVER use Pyomo's methods, function, and attributes, + because the code snippet MUST be reviewed by various researchers without Pyomo expertise + e.g. when iterating over components in , NEVER use a for-loop and ```model.component_map``` (Pyomo's method) + ONLY use the following generic shortcut functions to interact with + __SHORTCUT_FUNCTIONS_PLACEHOLDER__ +`code_rag` & `paper_rag` conventions + - ONLY used in the end: + only when have been thoroughly analyzed with PRIOR KNOWLEDGE, + the code blocks and paper contents are version-agnostic and can ONLY serve as supplementary information + prioritize using `get_model_components` and `python_repl_func` first + +RESPONSE STYLE +- coherent and information-grounded narrative +- NEVER be obsessed with calculating statistics and verifying user's observations +- focus on **explanations and analysis** to answer the USER QUERY +- NEVER do extra work. NEVER explore randomly. +""" + + +def get_expert_agent_prompt(prompt_version=1): + EXPERT_AGENT_PROMPTS = {1: EXPERT_AGENT_PROMPT_NO_SC, + 2: EXPERT_AGENT_PROMPT} + if prompt_version in EXPERT_AGENT_PROMPTS: + prompt = EXPERT_AGENT_PROMPTS[prompt_version] + else: + raise NotImplementedError(f"Prompt version '{prompt_version}' is not implemented.") + + shortcut_functions_docs = auto_extract_function_docs("optichat.tools.shortcut_functions") + + if prompt_version in [1, 2]: + prompt = prompt.replace("__SHORTCUT_FUNCTIONS_PLACEHOLDER__", shortcut_functions_docs) + + return prompt \ No newline at end of file diff --git a/optichat/sub_agents/root/agent.py b/optichat/sub_agents/root/agent.py new file mode 100644 index 0000000..e633c19 --- /dev/null +++ b/optichat/sub_agents/root/agent.py @@ -0,0 +1,25 @@ +from google.adk.agents import LlmAgent, BaseAgent, LoopAgent, SequentialAgent, ParallelAgent, Agent +from google.adk.tools.agent_tool import AgentTool +from optichat.llm import * +from optichat.config.constants import OUTPUT_KEY_ROOT_AGENT +from optichat.sub_agents.root.prompt import * +from optichat.sub_agents.expert.agent import create_expert_agent +from optichat.tools.callback_tool import (initialize_session, check_llm_request, + check_llm_response) + + +def create_root_agent(workflow="default"): + if workflow == "default": + expert_agent = create_expert_agent(prompt_version=1, tools_version=1) + root_agent = Agent(name="root_agent", + model=gpt_5_nano, + tools=[AgentTool(expert_agent)], + description="first point of contact for all user queries", + instruction=ROOT_AGENT_PROMPT, + output_key=OUTPUT_KEY_ROOT_AGENT, + before_agent_callback=initialize_session, + before_model_callback=check_llm_request, + after_model_callback=check_llm_response) + return root_agent + else: + raise NotImplementedError(f"Workflow '{workflow}' is not implemented.") \ No newline at end of file diff --git a/optichat/sub_agents/root/prompt.py b/optichat/sub_agents/root/prompt.py new file mode 100644 index 0000000..d9a9acc --- /dev/null +++ b/optichat/sub_agents/root/prompt.py @@ -0,0 +1,29 @@ +ROOT_AGENT_PROMPT = """ +You're the root agent, a coordinator between the user and sub-agents. +Your task is to understand the user's queries and delegate them to the appropriate sub-agents for processing if applicable. + +RESOURCES + (Dynamic Availability: {IS_MODELS_DICTIONARY_AVAILABLE}): + The optimization models labelled with version names {MODEL_VERSIONS}. + + (Dynamic Availability: {IS_MODELS_CODE_AVAILABLE}): + Code used to implement the optimization models. + + (Dynamic Availability: {IS_MODELS_PAPER_AVAILABLE}): + Scientific papers associated with the optimization models. + +TOOLS +`expert_agent` + Resource access: , , + +RESPONSE STYLE +information-grounded response is preferred. +- the `expert_agent` gather trustworthy and technical information from the available resources. +if the query requires deep analysis and explanation, use the `expert_agent` first. +NEVER speculate yourself. NEVER make up information yourself. + +user-friendly response is preferred. +- the user has little knowledge of optimization and operations research, but is familiar with the problem context that +the optimization models are designed for. +Avoid jargon, complex terminology without explanations, and overwhelming mathematical expressions and code snippets. +""" \ No newline at end of file diff --git a/optichat/tools/callback_tool.py b/optichat/tools/callback_tool.py new file mode 100644 index 0000000..3b4f07e --- /dev/null +++ b/optichat/tools/callback_tool.py @@ -0,0 +1,273 @@ +import time +import json +import os +import glob +from xml.parsers.expat import model +import tiktoken +from loguru import logger +from typing import Dict, Any, List +from typing import Optional +from copy import deepcopy +from google.genai import types +from google.adk.agents.callback_context import CallbackContext +from google.adk.tools.tool_context import ToolContext +from google.adk.tools.base_tool import BaseTool +from google.adk.models import LlmResponse, LlmRequest +from optichat.config.constants import (IS_SESSION_INITIALIZED, PERSISTENT_STATES, TEMPORARY_STATES, + CFG, IS_EXPERT_AGENT_USED, EXPERT_AGENT_START_TIME, + MODELS_DICTIONARY, MODEL_VERSIONS, IS_MODELS_DICTIONARY_AVAILABLE, + IS_MODELS_CODE_AVAILABLE, IS_MODELS_PAPER_AVAILABLE, USER_QUERY) +from optichat.tools.extract_tool import restore_model_object, save_model_object, extract_model_info, _solve_model +from optichat.tools.rag_tool import init_paper_rag, init_code_rag + + +def initialize_session(callback_context: CallbackContext): + if IS_SESSION_INITIALIZED not in callback_context.state: + callback_context.state.update(PERSISTENT_STATES) # which also set IS_SESSION_INITIALIZED as False + callback_context.state.update(TEMPORARY_STATES) + user_content = callback_context.user_content + user_query = callback_context.user_content.parts[0].text + parts_wo_json = [] + for part in user_content.parts: + if getattr(part, "inline_data", None) is not None: + if part.inline_data.mime_type == "application/json": + if callback_context.state[IS_SESSION_INITIALIZED]: + raise NotImplementedError("Session is already initialized, cannot re-initialize with a new cfg. Open a new session instead.") + else: + raw = part.inline_data.data + cfg = json.loads(raw.decode("utf-8")) + cfg = _init_cfg(cfg) + callback_context.state[CFG] = cfg + models_dictionary, model_versions = _init_models(cfg) + is_model_dictionary_available = len(models_dictionary) > 0 + callback_context.state[MODELS_DICTIONARY] = models_dictionary + callback_context.state[MODEL_VERSIONS] = model_versions + callback_context.state[IS_MODELS_DICTIONARY_AVAILABLE] = is_model_dictionary_available + is_models_code_available = _init_models_code(cfg) + callback_context.state[IS_MODELS_CODE_AVAILABLE] = is_models_code_available + is_models_paper_available = _init_models_paper(cfg) + callback_context.state[IS_MODELS_PAPER_AVAILABLE] = is_models_paper_available + callback_context.state[IS_SESSION_INITIALIZED] = True + else: + parts_wo_json.append(part) + else: + parts_wo_json.append(part) + # replace user_content with parts without json part (if a part has json, it cannot be processed) + callback_context.user_content.parts = parts_wo_json + # reset temporary states for every query + callback_context.state.update(TEMPORARY_STATES) + callback_context.state[USER_QUERY] = user_query + return None + + +def _init_models(cfg: dict): + models_dictionary = {} + model_versions = [] + if "models" in cfg: + is_solved = cfg["models"].get("is_solved", False) + is_lp = cfg["models"].get("is_lp", False) + for resource_path in cfg["models"].get("local_resources", []): + model, version = restore_model_object(resource_path) + model, termination_condition = _solve_model(model, is_lp=is_lp, is_solved=is_solved) + info = extract_model_info(model, termination_condition=termination_condition) + local_path_to_object = save_model_object(model, version) + info.update({"local_path_to_object": local_path_to_object,}) + models_dictionary.update({version: info}) + model_versions.append(version) + return models_dictionary, model_versions + + +def _init_models_code(cfg: dict): + if "models_code" in cfg: + paths = cfg["models_code"].get("local_resources", []) + model_name = cfg.get("model_name", "default_model") + # TODO: temporary solution to use GenericLoader in rag_tool.py, which only supports one path + init_code_rag(paths[0], model_name) + is_models_code_available = True + else: + logger.debug("No 'models_code' in cfg") + is_models_code_available = False + return is_models_code_available + + +def _init_models_paper(cfg: dict): + if "models_paper" in cfg: + paths = cfg["models_paper"].get("local_resources", []) + model_name = cfg.get("model_name", "default_model") + init_paper_rag(paths, model_name) + is_models_paper_available = True + else: + logger.debug("No 'models_paper' in cfg") + is_models_paper_available = False + return is_models_paper_available + + +def _init_cfg(cfg: dict): + cfg_out = deepcopy(cfg) + + required_sections = ["models", "models_code", "models_paper"] + extension_filters = { + "models": [".pkl"], + "models_code": [".py"], + "models_paper": [".txt", ".pdf"] + } + + for section_key, section_cfg in cfg.items(): + if section_key in required_sections: + local_resources = section_cfg.get("local_resources", []) + allowed_extensions = extension_filters.get(section_key, None) + # TODO: temporary solution to use init_code_rag() in rag_tool.py + if section_key == "models_code": + logger.warning(("For 'models_code', ONLY one path is supported for now, " + "which must be a .py file or a wildcard path to indicate a folder. " + "No expand_resources() is performed for 'models_code' in cfg.")) + assert len(local_resources) == 1, "ONLY one path for models_code is supported for now." + assert local_resources[0].endswith(".py") or local_resources[0].endswith("*"), "models_code path must be a .py file or a wildcard path." + else: + expanded_resources = _expand_resources(local_resources, allowed_extensions) + cfg_out[section_key]["local_resources"] = expanded_resources + return cfg_out + + +def _expand_resources(local_resources: List[str], allowed_extensions: Optional[List[str]]): + """ + Expand wildcard * patterns with allowed extensions filtering + """ + expanded_resources = [] + for resource_path in local_resources: + if "*" in resource_path: + matches = glob.glob(resource_path) + else: + matches = [resource_path] + if not matches: + raise FileNotFoundError(f"No files matched: {resource_path}") + if allowed_extensions: + filtered_matches = [ + match for match in matches + if any(match.lower().endswith(ext) for ext in allowed_extensions) + ] + else: + filtered_matches = matches + expanded_resources.extend(sorted(filtered_matches)) + return expanded_resources + + +def check_is_expert_agent_used(callback_context: CallbackContext): + is_expert_agent_used = callback_context.state.get(IS_EXPERT_AGENT_USED, None) + if is_expert_agent_used is None: + raise ValueError("check_is_expert_agent_used: IS_EXPERT_AGENT_USED is not set in the state.") + + if is_expert_agent_used: + return types.Content( + parts=[types.Part(text=f"[system message]: Expert agent has already been used. " + f"Expert agent can ONLY be used once per user query. " + f"Explain the last response from expert agent to the user first. ")], + role="model" + ) + else: + callback_context.state[IS_EXPERT_AGENT_USED] = True + callback_context.state[EXPERT_AGENT_START_TIME] = time.time() + return None + + +def check_expert_agent_runtime(callback_context: CallbackContext): + is_expert_agent_used = callback_context.state.get(IS_EXPERT_AGENT_USED) + if is_expert_agent_used: + start_time = callback_context.state.get(EXPERT_AGENT_START_TIME) + if start_time: + elapsed_time = time.time() - start_time + logger.debug(f"*** Expert Agent Runtime: {elapsed_time:.2f} s " + f"({elapsed_time/60:.2f} min) ***") + return None + + +def check_llm_request(callback_context: CallbackContext, llm_request: LlmRequest): + agent_name = callback_context.agent_name + original_instruction = llm_request.config.system_instruction or types.Content(role="system", parts=[]) + # Ensure system_instruction is Content and parts list exists + if not isinstance(original_instruction, types.Content): + # Handle case where it might be a string (though config expects Content) + original_instruction = types.Content(role="system", parts=[types.Part(text=str(original_instruction))]) + if not original_instruction.parts: + original_instruction.parts.append(types.Part(text="")) # Add an empty part if none exist + + original_text = original_instruction.parts[0].text or "" + logger.info((f"[Callback] Inspecting LLM request from '{agent_name}': " + f"{original_text}")) + return None + + +def check_llm_response(callback_context: CallbackContext, llm_response: LlmResponse): + agent_name = callback_context.agent_name + if llm_response.content and llm_response.content.parts: + if llm_response.content.parts[0].text: + original_text = llm_response.content.parts[0].text + logger.info((f"[Callback] Inspecting LLM response from '{agent_name}': " + f"{original_text}")) + elif llm_response.content.parts[0].function_call: + logger.info((f"[Callback] Inspecting LLM function call from '{agent_name}': " + f"{llm_response.content.parts[0].function_call.name}")) + else: + logger.info("[Callback] Inspected LLM response: No text content found.") + elif llm_response.error_message: + logger.error((f"[Callback] Inspected LLM response: " + f"Contains error '{llm_response.error_message}'. ")) + else: + logger.warning("[Callback] Inspected LLM response: Empty LlmResponse.") + return None + + +def check_tool_usage(tool: BaseTool, args: Dict[str, Any], tool_context: ToolContext): + agent_name = tool_context.agent_name + tool_name = tool.name + + usage_key = f"{agent_name.upper()}_{tool_name.upper()}_USES" + if usage_key in tool_context.state: + uses_left = tool_context.state[usage_key] + if uses_left <= 0: + logger.debug(f"Usage key '{usage_key}' has no remaining uses.") + return {"result": f"\n[system message]: **WARNING** '{tool_name}' tool cannot be used anymore! "} + else: + tool_context.state[usage_key] -= 1 + logger.debug(f"Usage key '{usage_key}' decremented. Remaining uses: {tool_context.state[usage_key]}") + else: + logger.debug(f"Usage key '{usage_key}' not found in the state. Skipping tool usage check.") + return None + + +def check_tool_response(tool: BaseTool, + args: Dict[str, Any], + tool_context: ToolContext, + tool_response: Dict): + show_first_n_chars = 500 + agent_name = tool_context.agent_name + tool_name = tool.name + # AgentTool may return str instead of Dict as tool_response + result = tool_response.get("result", "") + max_tokens_key = f"{agent_name.upper()}_{tool_name.upper()}_MAX_TOKENS" + if max_tokens_key in tool_context.state: + max_tokens = tool_context.state[max_tokens_key] + try: + encoding = tiktoken.get_encoding("cl100k_base") + tokens = encoding.encode(result) + token_count = len(tokens) + if token_count > max_tokens: + # truncate the result to max_tokens + truncated_tokens = tokens[:max_tokens] + truncated_result = encoding.decode(truncated_tokens) + truncated_result += ("... \n[system message]: **WARNING** " + "Execution result was truncated due to token limit.") + logger.warning((f"'{tool_name.upper()}' execution (truncated) result: {truncated_result[:show_first_n_chars]}" + "\n... (showing only the first {show_first_n_chars} characters)")) + # return a truncated tool_response dictionary + truncated_tool_response = deepcopy(tool_response) + truncated_tool_response["result"] = truncated_result + return truncated_tool_response + except Exception as e: + raise RuntimeError(f"Token counting failed: {e}.") + else: + logger.debug(f"max tokens key '{max_tokens_key}' not found in the state. Skipping tool response check.") + logger.info(f"'{tool_name.upper()}' execution result: {result[:show_first_n_chars]}" + f"\n... (showing only the first {show_first_n_chars} characters)") + return None # Return None to indicate no modification to tool_response + diff --git a/optichat/tools/custom_tool.py b/optichat/tools/custom_tool.py new file mode 100644 index 0000000..743a88b --- /dev/null +++ b/optichat/tools/custom_tool.py @@ -0,0 +1,784 @@ +from __future__ import annotations +from typing import Any, Dict, List, Optional +import os, shutil, tempfile, subprocess +from loguru import logger + +import pyomo.environ as pyo +from pyomo.contrib.iis import write_iis +from pyomo.opt import SolverFactory, SolverStatus, TerminationCondition + +from google.adk.tools.tool_context import ToolContext +from optichat.tools.shortcut_functions import load_model, solve_model, parse_uncertainty_from_state +from optichat.tools.extract_tool import unique_component_name +from optichat.config.constants import MODELS_DICTIONARY, MODEL_VERSIONS + +#LDR +from optichat.tools.ldr_explain import core as ldr_core +from optichat.tools.ldr_explain import extractor as ldr_extractor + +#Robust Analysis +from optichat.tools.robust_analysis import scenario_generator as robust_scenarios +from optichat.tools.robust_analysis import robustness_analysis as robust_core + + + +# ========================= +# Helpers +# ========================= + +def _json_safe(obj: Any) -> Any: + """ + Convert pandas/numpy outputs to plain-JSON types for the LLM. + - DataFrame -> {"schema":{"columns":[...], "rows":N}, "records":[{...}, ...]} + - numpy scalars -> builtins via .item() + - numpy arrays / Series -> .tolist() + - containers -> recurse + - else -> str(obj) + """ + # pandas.DataFrame (duck-typed) + if hasattr(obj, "to_dict") and hasattr(obj, "columns") and hasattr(obj, "shape"): + records = obj.to_dict(orient="records") + records = [_json_safe(r) for r in records] + cols = [str(c) for c in list(obj.columns)] + return {"schema": {"columns": cols, "rows": int(obj.shape[0])}, "records": records} + + # numpy scalar + if hasattr(obj, "item") and callable(getattr(obj, "item", None)): + try: + return obj.item() + except Exception: + pass + + # numpy array / pandas Series + if hasattr(obj, "tolist") and callable(getattr(obj, "tolist", None)): + try: + return obj.tolist() + except Exception: + pass + + if isinstance(obj, dict): + return {str(k): _json_safe(v) for k, v in obj.items()} + if isinstance(obj, (list, tuple)): + return type(obj)(_json_safe(v) for v in obj) + if isinstance(obj, (str, int, float, bool)) or obj is None: + return obj + return str(obj) + + +def write_lp_with_symbolic_names(model: pyo.ConcreteModel, lp_path: str) -> None: + """ + Brief: Write an LP file with symbolic labels so IIS entries match Pyomo names. + + Operations: + 1) model.write(lp_path, io_options={'symbolic_solver_labels': True}) + Returns: + None + """ + model.write(lp_path, io_options={"symbolic_solver_labels": True}) + + +def run_gurobi_cli_iis(lp_path: str, workdir: Optional[str] = None) -> Optional[str]: + """ + Brief: Request IIS via Gurobi CLI with DualReductions=0 (robust for INF_OR_UNBD). + + Operations: + 1) Call: gurobi_cl DualReductions=0 IIS=1 + 2) Return the generated .ilp path if found, else None + Returns: + str | None + """ + exe = shutil.which("gurobi_cl") + if exe is None: + return None + wd = workdir or os.path.dirname(os.path.abspath(lp_path)) or "." + try: + cmd = [exe, "DualReductions=0", "IIS=1", os.path.abspath(lp_path)] + subprocess.run(cmd, cwd=wd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) + base, _ = os.path.splitext(os.path.basename(lp_path)) + candidate = os.path.join(wd, f"{base}.ilp") + return candidate if os.path.exists(candidate) else None + except Exception: + return None + + +def iis2json(lp_like_path: str) -> Dict[str, List[str]]: + """ + Extract the constraint names between 'Subject To' section and the next section. + + Operations: + 1) Read text + 2) Extract labels ':' within 'Subject To' block + 3) Deduplicate in order + Returns: + {"constraints": [str, ...]} + """ + txt = open(lp_like_path, "r", encoding="utf-8", errors="replace").read() + + capture = False + block_lines: List[str] = [] + for raw_line in txt.splitlines(): + stripped = raw_line.strip() + lower = stripped.lower() + + if not capture: + if lower.startswith("subject to"): + capture = True + idx = lower.find("subject to") + remainder = raw_line[idx + len("subject to"):].strip() + if remainder: + block_lines.append(remainder) + continue + + if lower.startswith(("bounds", "binaries", "binary", "generals", "general", "end")): + break + block_lines.append(raw_line) + + if not block_lines: + block_lines = txt.splitlines() + + names: List[str] = [] + for raw_line in block_lines: + line = raw_line.strip() + if not line or line.startswith("\\"): + continue + + # Capture everything before the first ':'; IIS writers use that portion as the label. + if ":" not in line: + continue + candidate = line.split(":", 1)[0].strip() + if not candidate: + continue + + # Gurobi may quote names with single/double quotes; remove them for consistency. + candidate = candidate.strip("'\"") + candidate = candidate.replace("(", "[").replace(")", "]") + names.append(candidate) + + seen, ordered = set(), [] + for n in names: + if n not in seen: + seen.add(n) + ordered.append(n) + return {"constraints": ordered} + + +def append_iis_history(version: str, models_dictionary: Dict[str, Any], record: Dict[str, Any]) -> None: + """Add an IIS record to the registry entry for this version.""" + entry = models_dictionary.get(version, {}) + history = entry.get("iis_history", []) + history.append(record) + entry["iis_history"] = history + models_dictionary[version] = entry + + +def append_repairs_applied(version: str, models_dictionary: Dict[str, Any], record: Dict[str, Any]) -> None: + """Add a restoration record to the registry entry for this version.""" + entry = models_dictionary.get(version, {}) + repairs = entry.get("repairs_applied", []) + repairs.append(record) + entry["repairs_applied"] = repairs + models_dictionary[version] = entry + + +# Infeasibility Diagnosis + +def infeasibility_diagnosis( + version: str, + tool_context: ToolContext +) -> str: + """ + infeasibility_diagnosis is a tool that performs infeasibility diagnosis on a specified model version + + Args: + version (str): Model version to perform infeasibility diagnosis on. + Returns: + Dict[str, str]: a dictionary with two keys: "status" and "result" + "status": "success" or "error" + "result": a report about the Irreducible Infeasible Subsystem (IIS) that represent the minimal set of constraints causing infeasibility, + and corresponding recommendations for feasibility restoration. + """ + # TODO: add these keys to constants when more options are considered + + # Checking if the mdoel verison exists + if version not in tool_context.state.get(MODEL_VERSIONS, []): + return {"status": "error", "result": f"Model version '{version}' not found in tool_context.state. Specify the right version"} + + solver_name = tool_context.state.get("SOLVER_NAME", "gurobi") + solver_options = tool_context.state.get("SOLVER_OPTIONS", None) + tee = bool(tool_context.state.get("SOLVE_TEE", False)) + save_iis_dir = tool_context.state.get("IIS_SAVE_DIR", os.path.join("tmp", "iis", version)) + os.makedirs(save_iis_dir, exist_ok=True) + + # solve if not solved yet + models_dictionary = tool_context.state[MODELS_DICTIONARY].copy() + if models_dictionary.get(version, {}).get("obj", {}).get("sol_status", "unknown") == "unknown": + model = load_model(version, models_dictionary) + models_dictionary = solve_model(model, version, models_dictionary) + tool_context.state[MODELS_DICTIONARY] = models_dictionary + + models_dictionary = tool_context.state[MODELS_DICTIONARY].copy() + model = load_model(version, models_dictionary) + info = models_dictionary.get(version, {}).get("obj", {}) + status = info.get("sol_status", "unknown") + objval = info.get("value", "unknown") + # stop if NOT infeasible + if status not in [TerminationCondition.infeasible, TerminationCondition.infeasibleOrUnbounded]: + return {"status": "success", "result": "Model is NOT infeasible; infeasibility diagnosis terminated directly."} + # Produce IIS (robust path, then fallback) + with tempfile.TemporaryDirectory() as td: + lp_path = os.path.join(td, "model.lp") + write_lp_with_symbolic_names(model, lp_path) + + iis_path = run_gurobi_cli_iis(lp_path, workdir=td) + if iis_path is None or not os.path.exists(iis_path): + iis_path = os.path.join(td, "fallback.iis.ilp") + try: + write_iis(model, iis_path, solver=solver_name) + except Exception as e: + append_iis_history( + version, + models_dictionary, + { + "supported": False, + "summary": f"IIS could not be generated: {e}", + "constraints": [], + "artifact_path": None, + "solve": {"status": status, "objective_value": objval}, + }, + ) + tool_context.state[MODELS_DICTIONARY] = models_dictionary + logger.error(f"write_iis failed: {e}") + return {"status": "error", + "result": f"Model is infeasible, but write_iis (internal function) failed: {e}"} + + parsed = iis2json(iis_path) + constraints = parsed.get("constraints", []) + + final_artifact = None + if save_iis_dir: + try: + final_artifact = os.path.join(save_iis_dir, "iis.ilp") + shutil.copyfile(iis_path, final_artifact) + except Exception: + final_artifact = None + + iis_record = { + "supported": True, + "summary": f"IIS includes {len(constraints)} constraint(s).", + "constraints": constraints, + "artifact_path": final_artifact, + "solve": {"status": status, "objective_value": objval}, + } + append_iis_history(version, models_dictionary, iis_record) + + tool_context.state[MODELS_DICTIONARY] = models_dictionary + + if constraints: + lines = [ + f"IIS includes {len(constraints)} constraint(s): ", + ] + constraints + return {"status": "success", "result": "\n".join(lines)} + else: + logger.warning("No constraints parsed from IIS artifact.") + return {"status": "error", "result": "\nNo constraints parsed from IIS artifact. iis2json might be problematic."} + + +# Linear Decision Rule Functions + +def ldr_model_generator( + version: str, + uncertain_params: Optional[List[str]] = None, + bounds: Optional[Dict[str, tuple]] = None, + tool_context: ToolContext = None, +) -> Dict[str, Any]: + """ + Build primal/dual LDRs for a FEASIBLE base model `version`. + - No autosolve; we only use cached status and bail if not feasible. + - If `uncertain_params` / `bounds` missing, parse them from the latest user message in state. + - Stores derived models as {version}__ldr_primal / {version}__ldr_dual and attaches a compact summary to the base. + """ + + state = tool_context.state + md = state[MODELS_DICTIONARY].copy() + if md.get(version, {}).get("obj", {}).get("sol_status", "unknown") in [TerminationCondition.infeasible, TerminationCondition.infeasibleOrUnbounded]: + return { + "status": "error", + "result": f"Base model version '{version}' is not feasible; LDR generation aborted." + } + + base_model = load_model(version, md) + + # ==== Uncertainty spec (from args or user message) ==== + # if uncertain_params is None or bounds is None: + # up_auto, b_auto = parse_uncertainty_from_state(state) + # if uncertain_params is None: + # uncertain_params = up_auto + # if bounds is None: + # bounds = b_auto + + # if not uncertain_params or not bounds: + # return { + # "status": "error", + # "result": "Missing 'uncertain_params' and/or 'bounds'. " + # "Pass them as tool args or include a JSON block / inline spec in your message." + # } + + uncertain_params = ["demand[1,1]", "demand[2,1]"] + bounds = [(12, 18), (10, 20)] + + # ==== LDR core entrypoint check ==== + Core = getattr(ldr_core, "LDRPrimalDualCore", ldr_core) + target = getattr(Core, "build_extract_solve_both", None) + if target is None or not callable(target): + raise RuntimeError("LDR core is missing 'build_extract_solve_both'.") + + # Decide whether to pass 'param_box' or 'bounds' without try/except + code = getattr(target, "__code__", None) + varnames = set(code.co_varnames) if code is not None else set() + use_param_box = "param_box" in varnames + use_bounds_kw = "bounds" in varnames + + kwargs = {"base_model": base_model, "uncertain_params": uncertain_params, "param_box": bounds, "xi_set": pyo.RangeSet(1, len(uncertain_params) +1), + "bounds": bounds, "return_models": True, "tee": False} + # if use_param_box: + # kwargs["param_box"] = bounds + # elif use_bounds_kw: + # kwargs["bounds"] = bounds + # else: + # raise RuntimeError("LDR core 'build_extract_solve_both' expects 'param_box' or 'bounds' keyword.") + + res = target(**kwargs) + + # ==== Unpack result (dict / tuple / attribute) ==== + primal_model = dual_model = None + obj_primal = obj_dual = None + + primal_model = res.get("primal_ldr") + dual_model = res.get("dual_ldr") + obj_primal = res.get("primal_obj") + obj_dual = res.get("dual_obj") + gap = res.get("gap") + primal_model_status = res.get("primal_status").get("termination") + dual_model_status = res.get("dual_status").get("termination") + + if primal_model is None or dual_model is None: + raise RuntimeError("LDR core did not return recognized 'primal_model' and 'dual_model'.") + + # --- persist minimal LDR entries --- + p_ver = f"{version}__ldr_primal" + d_ver = f"{version}__ldr_dual" + + md[p_ver] = { + "model": primal_model, + "parent_version": version, + "role": "ldr_primal", + "sol_status": "optimal", + "is_ldr": True, + } + md[d_ver] = { + "model": dual_model, + "parent_version": version, + "role": "ldr_dual", + "sol_status": "optimal", + "is_ldr": True, + } + + # state[MODELS_DICTIONARY] = md + + msg = ( + f"LDR generated for '{version}'. " + f"Primal obj={obj_primal}, Dual obj={obj_dual}, " + f"Gap(abs)={gap}. " + ) + return {"status": "success", "result": msg} + +def ldr_expression_generator( + version: str, + variable: Optional[str] = None, + side: Optional[str] = None, # 'primal' | 'dual' | None + tool_context: ToolContext = None, +) -> Dict[str, Any]: + """ + Return the LDR expression text for `variable`. + - `version` may be the base version OR an LDR-derived version (…__ldr_primal / …__ldr_dual). + - If `side` omitted, defaults to 'primal'. If version suffix implies side, that wins. + - Uses tools/ldr_explainer/extractor.py if available; otherwise falls back to Pyomo string. + """ + state = tool_context.state + md: Dict[str, Any] = state.get(MODELS_DICTIONARY, {}) + entry = md.get(version) + + # Map base version to LDR version if needed + if entry is None and version in md and md[version].get("is_ldr") is not True: + lsum = md[version].get("ldr", {}).get("summary", {}) + target_ver = lsum.get("primal_version") + if (side or "").lower() == "dual": + target_ver = lsum.get("dual_version") or target_ver + version = target_ver or version + entry = md.get(version) + + if entry is None: + return {"status": "error", "result": f"Version '{version}' not found in the registry."} + if not variable: + return {"status": "error", "result": "Variable name was not provided."} + + # Decide side + side_txt = (side or "primal").lower() + if version.endswith("__ldr_primal"): + side_txt = "primal" + if version.endswith("__ldr_dual"): + side_txt = "dual" + + model = entry["model"] + + # Preferred extractor function name order + candidates = ( + "ldr_expression", + "get_ldr_expression", + "expression_for", + "generate_expression", + ) + fn = None + for name in candidates: + cand = getattr(ldr_extractor, name, None) + if callable(cand): + fn = cand + break + if fn is None: + raise RuntimeError("LDR extractor has no suitable expression function.") + + # Call extractor in a single, explicit way (no try/except) + # Expected signature: (model=..., var_name=..., side=...) + if "var_name" in getattr(fn, "__code__", None).co_varnames: + expr_text = fn(model=model, var_name=variable, side=side_txt) + elif "variable" in getattr(fn, "__code__", None).co_varnames: + expr_text = fn(model=model, variable=variable, side=side_txt) + else: + raise RuntimeError("LDR extractor expression function must accept 'var_name' or 'variable'.") + + # Fallback: if extractor returns None/empty, produce a generic Pyomo representation + if not expr_text: + base = variable.split("[", 1)[0] + comp = getattr(model, base) # will raise AttributeError if missing — as desired + expr_text = str(comp) + + return { + "status": "success", + "result": f"LDR {side_txt} expression for {variable} (version={version}):\n{expr_text}", + "data": {"version": version, "side": side_txt, "variable": variable, "expression": str(expr_text)}, + } + + +# Robustness Analysis +def robustness_analysis( + version: str, + tool_context: ToolContext = None, + n_scenarios: int = 10, +) -> Dict[str, Any]: + """ + Generate uniform scenarios and run robustness analysis on a FEASIBLE base model `version`. + - No state/registry updates. + - Returns JSON-safe payload (DataFrame -> records), suitable for LLM consumption. + - `n_scenarios` defaults to 10; CSV path is intentionally unsupported here. + """ + state = tool_context.state + md = state[MODELS_DICTIONARY].copy() + + # Feasibility guard (mirrors your LDR style) + status_in_obj = md.get(version, {}).get("obj", {}).get("sol_status", "unknown") + if ( + status_in_obj in [TerminationCondition.infeasible, TerminationCondition.infeasibleOrUnbounded] + or (isinstance(status_in_obj, str) and status_in_obj.lower() in {"infeasible", "infeasibleorunbounded"}) + ): + return { + "status": "error", + "result": f"Base model version '{version}' is not feasible; robustness analysis aborted." + } + + base_model = load_model(version, md) + + # # --- Scenario generation (uniform by default) --- + # if hasattr(robust_scenarios, "generate_scenarios_from_model"): + # scen_fn = robust_scenarios.generate_scenarios_from_model + # scenarios_df = scen_fn(uncertain_params = ["demand[1,1]", "demand[2,1]"], bounds = [(12, 18), (10, 20)], n = n_scenarios) + # else: + # raise RuntimeError( + # "robust_analysis.scenario_generator has no supported entrypoint: " + # "expected 'generate_uniform_scenarios' or 'generate_scenarios'." + # ) + + # print(scenarios_df) # Works till here, perfect + + # --- Robustness analysis --- + if hasattr(robust_core, "run_robustness"): + robust_function = getattr(robust_core, "run_robustness") + else: + raise RuntimeError( + "robust_analysis.robustness_analysis has no supported entrypoint " + "Missing run_robustness function" + ) + + robust_df = robust_function(model = base_model, uncertain_params = [base_model.demand[1,1], base_model.demand[2,1]], bounds = [(12, 18), (10, 20)], + n_scenarios = n_scenarios, dist = "uniform") + + print(robust_df) + + # --- JSON-safe return (no Pyomo / pandas objects in payload) --- + js = _json_safe(robust_df) + rows = 0 + try: + rows = int(js.get("schema", {}).get("rows", 0)) + except Exception: + pass + + return { + "status": "success", + "result": f"Robustness analysis (uniform, {n_scenarios} scenarios) completed for '{version}'. Rows: {rows}.", + "data": js, + } + + + +# Feasibility Restoration + +# def feasibility_restoration( +# version: str, +# recommendation: Dict[str, Any], +# slack_penalty: float, +# tool_context: ToolContext, +# ) -> str: +# """ +# Brief: Apply a single IIS-based restoration by adding penalized slack to the target constraint; update registry and re-solve. + +# Operations: +# 1) Identify the active objective and compute penalty sign (min/max). +# 2) Locate target constraint by name; deactivate it and add a relaxed copy with nonnegative slack. +# 3) Add penalty term to the objective; append restoration record; re-solve and persist registry. + +# Returns: +# "Feedback from internal tools:\\n..." (plain text). +# """ +# if tool_context is None: +# return "Feedback from internal tools: \nMissing tool_context." + +# state = tool_context.state +# try: +# models_dictionary = state["MODELS_DICTIONARY"] +# except KeyError: +# return "Feedback from internal tools: \nMODELS_DICTIONARY not found in tool_context.state." + +# # Read solver configuration from state +# solver_name = state.get("SOLVER_NAME", "gurobi") +# solver_options = state.get("SOLVER_OPTIONS", None) +# tee = bool(state.get("SOLVE_TEE", False)) + +# # Load the current, live model instance from the registry +# model = load_model(version, models_dictionary) + +# # Active objective +# try: +# obj = next(model.component_data_objects(pyo.Objective, active=True)) +# except StopIteration: +# return "Feedback from internal tools: \nNo active objective to penalize." + +# is_min = (obj.sense == pyo.minimize) +# penalty_sign = 1.0 if is_min else -1.0 + +# if recommendation.get("type") != "constraint_slack": +# return "Feedback from internal tools: \nUnsupported recommendation type." + +# con_map = {c.name: c for c in model.component_data_objects(pyo.Constraint, active=True)} +# tname = recommendation.get("target") +# if tname not in con_map: +# return "Feedback from internal tools: \nConstraint not found: " + str(tname) + +# c = con_map[tname] +# safe = str(tname).replace("[", "_").replace("]", "").replace(",", "_").replace(" ", "_") + +# created = [] +# if c.equality(): +# s_pos = pyo.Var(domain=pyo.NonNegativeReals) +# s_neg = pyo.Var(domain=pyo.NonNegativeReals) +# name_spos = unique_component_name(model, f"fr_spos_{safe}") +# name_sneg = unique_component_name(model, f"fr_sneg_{safe}") +# model.add_component(name_spos, s_pos) +# model.add_component(name_sneg, s_neg) +# new_con = pyo.Constraint(expr=(c.body == pyo.value(c.lower) + s_pos - s_neg)) +# name_rel = unique_component_name(model, f"fr_relaxed_{safe}") +# model.add_component(name_rel, new_con) +# obj.set_value(obj.expr + penalty_sign * slack_penalty * (s_pos + s_neg)) +# created = [name_spos, name_sneg, name_rel] +# elif c.has_ub(): +# s = pyo.Var(domain=pyo.NonNegativeReals) +# name_s = unique_component_name(model, f"fr_s_{safe}") +# model.add_component(name_s, s) +# new_con = pyo.Constraint(expr=(c.body <= pyo.value(c.upper) + s)) +# name_rel = unique_component_name(model, f"fr_relaxed_{safe}") +# model.add_component(name_rel, new_con) +# obj.set_value(obj.expr + penalty_sign * slack_penalty * s) +# created = [name_s, name_rel] +# elif c.has_lb(): +# s = pyo.Var(domain=pyo.NonNegativeReals) +# name_s = unique_component_name(model, f"fr_s_{safe}") +# model.add_component(name_s, s) +# new_con = pyo.Constraint(expr=(c.body >= pyo.value(c.lower) - s)) +# name_rel = unique_component_name(model, f"fr_relaxed_{safe}") +# model.add_component(name_rel, new_con) +# obj.set_value(obj.expr + penalty_sign * slack_penalty * s) +# created = [name_s, name_rel] +# else: +# return "Feedback from internal tools: \nConstraint has no bound to relax." + +# c.deactivate() + +# entry = {"type": "constraint_slack", "target": tname, "created": created} +# append_repairs_applied(version, models_dictionary, entry) + +# # Re-solve (same model instance) and persist registry +# models_dictionary = solve_model( +# model, version, models_dictionary, +# solver_name=solver_name, solver_options=solver_options, tee=tee +# ) +# state["MODELS_DICTIONARY"] = models_dictionary + +# return "Feedback from internal tools: \n" + f"Applied restoration: added penalized slack to '{tname}'. Created: {', '.join(created)}." + + +# Iterative Infeasibility Restoration + +# def iterative_feasibility_restoration( +# version: str, +# max_iterations: int, +# slack_penalty: float, +# tool_context: ToolContext +# ) -> str: +# """ +# Brief: Iteratively diagnose infeasibility, apply the first IIS-based restoration, and repeat until feasible or capped. + +# Operations: +# 1) Read registry and config from application state. +# 2) Loop: solve → IIS (robust CLI first, fallback Pyomo IIS) → record IIS → apply first recommendation (penalized slack) → continue. +# 3) Persist registry after each step and return a per-iteration summary string. + +# Returns: +# "Feedback from internal tools:\\n..." (plain text summary). +# """ +# if tool_context is None: +# return "Feedback from internal tools: \nMissing tool_context." + +# state = tool_context.state +# try: +# models_dictionary = state["MODELS_DICTIONARY"] +# except KeyError: +# return "Feedback from internal tools: \nMODELS_DICTIONARY not found in tool_context.state." + +# solver_name = state.get("SOLVER_NAME", "gurobi") +# solver_options = state.get("SOLVER_OPTIONS", None) +# tee = bool(state.get("SOLVE_TEE", False)) +# save_iis_dir = state.get("IIS_SAVE_DIR", os.path.join("tmp", "iis", version)) + +# # Ensure save dir (best-effort) +# try: +# os.makedirs(save_iis_dir, exist_ok=True) +# except Exception: +# save_iis_dir = None + +# # Load model once; modifications (slacks) are applied to this same instance +# model = load_model(version, models_dictionary) + +# iteration_summaries: List[str] = [] +# for it in range(1, max_iterations + 1): +# # Solve & update registry +# models_dictionary = solve_model( +# model, version, models_dictionary, +# solver_name=solver_name, solver_options=solver_options, tee=tee +# ) +# info = models_dictionary.get(version, {}).get("obj", {}) +# status = str(info.get("sol_status", "unknown")).lower() +# objval = info.get("value", "unknown") + +# feasible_like = ("optimal" in status) or ("feasible" in status and "infeasible" not in status) +# if feasible_like: +# iteration_summaries.append(f"Iteration {it}: Model is feasible. Objective={objval}") +# state["MODELS_DICTIONARY"] = models_dictionary +# header = "Iterative restoration summary:" +# return "Feedback from internal tools: \n" + "\n".join([header] + iteration_summaries) + +# # IIS (robust path, then fallback) +# with tempfile.TemporaryDirectory() as td: +# lp_path = os.path.join(td, f"iter_{it}.lp") +# write_lp_with_symbolic_names(model, lp_path) + +# iis_path = run_gurobi_cli_iis(lp_path, workdir=td) +# if iis_path is None or not os.path.exists(iis_path): +# iis_path = os.path.join(td, f"iis_iter_{it}.ilp") +# try: +# write_iis(model, iis_path, solver=solver_name) +# except Exception as e: +# iis_record = { +# "supported": False, +# "summary": f"Iteration {it}: IIS could not be generated: {e}", +# "constraints": [], +# "artifact_path": None, +# "solve": {"status": status, "objective_value": objval}, +# "iteration": it, +# } +# append_iis_history(version, models_dictionary, iis_record) +# iteration_summaries.append(iis_record["summary"]) +# state["MODELS_DICTIONARY"] = models_dictionary +# header = "Iterative restoration summary:" +# return "Feedback from internal tools: \n" + "\n".join([header] + iteration_summaries) + +# parsed = iis2json(iis_path) +# constraints = parsed.get("constraints", []) +# artifact_copy = None +# if save_iis_dir: +# try: +# artifact_copy = os.path.join(save_iis_dir, f"iis_iter_{it}.ilp") +# shutil.copyfile(iis_path, artifact_copy) +# except Exception: +# artifact_copy = None + +# iis_record = { +# "supported": True, +# "summary": f"Iteration {it}: IIS has {len(constraints)} constraint(s).", +# "constraints": constraints, +# "artifact_path": artifact_copy, +# "solve": {"status": status, "objective_value": objval}, +# "iteration": it, +# } +# append_iis_history(version, models_dictionary, iis_record) + +# if not constraints: +# iteration_summaries.append(f"Iteration {it}: No IIS recommendations were produced.") +# state["MODELS_DICTIONARY"] = models_dictionary +# header = "Iterative restoration summary:" +# return "Feedback from internal tools: \n" + "\n".join([header] + iteration_summaries) + +# # Apply first recommendation using the same in-memory model +# first = {"type": "constraint_slack", "target": constraints[0]} +# fr_msg = feasibility_restoration( +# version=version, +# recommendation=first, +# slack_penalty=slack_penalty, +# tool_context=tool_context, +# ) + +# # Keep last line of FR message for compact summary +# iteration_summaries.append( +# f"Iteration {it}: Applied restoration on '{constraints[0]}'. {fr_msg.splitlines()[-1]}" +# ) + +# # Loop exhausted → last attempt and summary +# models_dictionary = solve_model( +# model, version, models_dictionary, +# solver_name=solver_name, solver_options=solver_options, tee=tee +# ) +# info = models_dictionary.get(version, {}).get("obj", {}) +# status = str(info.get("sol_status", "unknown")) +# iteration_summaries.append("Maximum iterations reached without achieving feasibility.") +# iteration_summaries.append(f"Last status: {status}") +# state["MODELS_DICTIONARY"] = models_dictionary + +# header = "Iterative restoration summary:" +# return "Feedback from internal tools: \n" + "\n".join([header] + iteration_summaries) diff --git a/optichat/tools/extract_tool.py b/optichat/tools/extract_tool.py new file mode 100644 index 0000000..f0cd34e --- /dev/null +++ b/optichat/tools/extract_tool.py @@ -0,0 +1,290 @@ +import pyomo.environ as pe +from pyomo.opt import SolverFactory, SolverStatus, TerminationCondition +import cloudpickle +import os +from optichat.config.constants import TMP_MODEL_OBJECT_FOLDER + + +def extract_expressions_from_lp(lp_local_file_path: str): + """ + Process the .lp file given by lp_local_file_path, + get all expressions (constraints and objective) from the LP file. + + **This function works for modelling languages that load and write .lp files, + not very applicable to Pyomo (as far as I know) + Therefore this is NOT USED for now.** + + Args: + lp_local_file_path(str): path to the local LP file + + Returns: + dict: {name: {'expression': expression, 'component_type': "objective" or "constraint"}}. + """ + info = {} + with open(lp_local_file_path, 'r') as f: + lp_content = f.read() + obj_keywords_to_check = ["Minimize", "Maximize"] + existing_obj_keywords = [k for k in obj_keywords_to_check if k in lp_content] + if len(existing_obj_keywords) > 1: + raise ValueError(f"The LP file contains both {obj_keywords_to_check} sections.") + elif len(existing_obj_keywords) == 1: + obj_keyword = existing_obj_keywords[0] + else: + raise ValueError(f"The LP file does not contain any of {obj_keywords_to_check} section.") + if "Subject To" not in lp_content: + raise ValueError("The LP file does not contain 'Subject To' section.") + + objective_start = lp_content.find(obj_keyword) + objective_end = lp_content.find("Subject To") + objective_section = lp_content[objective_start:objective_end].strip() + # Parse the objective expression + lines = objective_section.split('\n') + objective_expr = obj_keyword + for line in lines: + line = line.strip() + if line and not line.startswith('\\'): # Skip comment lines + if ":" in line: + # Remove the objective name part (e.g., "__OBJ__:") + expr_part = line[line.find(":") + 1:].strip() + if expr_part: + objective_expr += " " + expr_part + elif line.startswith(('+', '-')) or any(char.isdigit() for char in line): + # This is a continuation line with coefficients and variables + objective_expr += " " + line + # TODO: address the special characters parsed by .lp file (check .lp file's parser and parse it back) + parsed_objective_expr = objective_expr.replace("@2D", "-") + info["obj"] = {"expression": parsed_objective_expr.strip(), "component_type": "objective"} + + # Extract constraint expressions + sections = lp_content.split("Subject To")[1] + # TODO: currently NOT handle constraints in "Bounds", "General", "Binary", "End" sections + next_sections = ["Bounds", "General", "Binary", "End"] + for section in next_sections: + if section in sections: + constraints_section = sections.split(section)[0] + break + else: + constraints_section = sections + lines = constraints_section.strip().split('\n') + i = 0 + while i < len(lines): + line = lines[i].strip() + # Check if this line starts with a constraint name followed by a colon + if ":" in line: + constraint_name = line.split(":")[0].strip() + # Extract the expression part (after the colon) + expression = line[line.find(":") + 1:].strip() + # Continue reading lines until the next constraint or end of section is found + j = i + 1 + while j < len(lines) and ":" not in lines[j] and lines[j].strip(): + expression += " " + lines[j].strip() + j += 1 + # TODO: address the special characters parsed by .lp file + parsed_constraint_name = constraint_name.replace("@2D", "-") + parsed_constraint_expr = expression.replace("@2D", "-") + info[parsed_constraint_name] = {"expression": parsed_constraint_expr.strip(), + "component_type": "constraint"} + # update i to j to skip the lines that have been processed + i = j - 1 + i += 1 + return info + + +def extract_model_param(model, termination_condition): + """ + Extract (mutable) parameters from a Pyomo model. + Information includes name, component_type, value, TODO:is_RHS? + """ + param_info = {} + for param in model.component_objects(pe.Param, active=True): + if param.mutable: + for idx in param: + try: + v = param[idx].value + except Exception as e: + raise ValueError(f"Error accessing value of parameter {pe.name(param)} with index {idx}: {e}") + param_info[pe.name(param[idx])] = {"component_type": "parameter", + "value": param[idx].value} + return param_info + + +def extract_model_var(model, termination_condition): + """ + Extract variables from a Pyomo model. + Information includes name, component_type, solution. + """ + var_info = {} + for var in model.component_objects(pe.Var, active=True): + for idx in var: + if str(termination_condition) == 'optimal': + solution = var[idx].value + else: + solution = "unknown" + var_info[pe.name(var[idx])] = {"component_type": "variable", + "solution": solution} + return var_info + + +def extract_model_constraint(model, termination_condition): + """ + Extract constraints from a Pyomo model. + Information includes name, component_type, expression, dual, is_binding + """ + eps = 1e-5 + constraint_info = {} + for constraint in model.component_objects(pe.Constraint, active=True): + for idx in constraint: + try: + v = constraint[idx].expr + except Exception as e: + raise ValueError(f"Error accessing expression of constraint {pe.name(constraint)} with index {idx}: {e}") + + if str(termination_condition) == 'optimal': + if abs(constraint[idx].lslack()) < eps or abs(constraint[idx].uslack()) < eps: + is_binding = True + else: + is_binding = False + else: + is_binding = "unknown" + + if hasattr(model, "dual"): + dual = model.dual[constraint[idx]] + else: + dual = "unknown" + constraint_info[pe.name(constraint[idx])] = {"component_type": "constraint", + "expression": str(constraint[idx].expr), + "is_binding": is_binding, + "dual": dual} + return constraint_info + + +def extract_model_objective(model, termination_condition): + """ + Extract objectives from a Pyomo model. + Information includes name, component_type, expression (with sense: min/max), value. + """ + objective_info = {} + objectives = list(model.component_objects(pe.Objective, active=True)) + if len(objectives) > 1: + raise ValueError("The model has multiple objectives, which is not supported.") + else: + obj = objectives[0] + if obj.sense == pe.minimize: + obj_sense = "MINIMIZE: " + elif obj.sense == pe.maximize: + obj_sense = "MAXIMIZE: " + else: + raise ValueError(f"Unknown objective sense {obj.sense} for objective {pe.name(obj)}") + objective_info["obj"] = {"component_type": "objective", + "expression": obj_sense + str(obj.expr), + "sol_status": str(termination_condition), + "value": pe.value(obj) if str(termination_condition) == 'optimal' else "unknown"} + return objective_info + + +def extract_model_info(model, termination_condition='unknown'): + # parameters + param_info = extract_model_param(model, termination_condition) + # variables + var_info = extract_model_var(model, termination_condition) + # constraints + constraint_info = extract_model_constraint(model, termination_condition) + # objective + objective_info = extract_model_objective(model, termination_condition) + # combine all info + info = {**param_info, **var_info, **constraint_info, **objective_info} + return info + + +def _solve_model(model, is_lp=False, is_solved=False): + """ + solve the model, + if is_lp, add dual suffix for LP models to enable dual extraction in the future. + if is_solved, skip solving and return the model directly. + """ + if is_solved: + return model, TerminationCondition.optimal + if is_lp and not hasattr(model, "dual"): + model.dual = pe.Suffix(direction=pe.Suffix.IMPORT_EXPORT) + solver = SolverFactory('gurobi') + results = solver.solve(model, tee=False) + termination_condition = results.solver.termination_condition + return model, termination_condition + + +def restore_model_object(file_path): + """ + use cloudpickle to restore a Pyomo model object from a file. + Note that file_name without suffix. .pkl is returned as well. + """ + with open(file_path, mode='rb') as file: + model = cloudpickle.load(file) + file_name = os.path.splitext(os.path.basename(file_path))[0] + return model, file_name + + +def save_model_object(model, file_name): + """ + use cloudpickle to save a Pyomo model object to a file. + Note that file_name is without suffix .pkl + """ + folder_name = os.path.join(os.getcwd(), TMP_MODEL_OBJECT_FOLDER) + os.makedirs(folder_name, exist_ok=True) + local_path_to_object = os.path.join(folder_name, f"{file_name}.pkl") + + with open(local_path_to_object, mode='wb') as file: + cloudpickle.dump(model, file) + return local_path_to_object + + +def auto_extract_function_docs(module_path: str) -> str: + """ + Automatically extracts function documentation from docstrings + for all functions in the specified module path. + Returns formatted strings that can be directly used in prompts. + + Args: + module_path (str): Path to the module to extract docs + (e.g., 'optichat.tools.shortcut_functions'). + + Returns: + str: Formatted documentation strings extracted from docstrings. + """ + try: + # Import the module dynamically + import importlib + module = importlib.import_module(module_path) + import inspect + docs = [] + for name, obj in inspect.getmembers(module, inspect.isfunction): + # Skip private functions and utility functions + if name.startswith("_") or name in ["auto_extract_function_docs"]: + continue + # Skip functions not defined in the target module + if obj.__module__ != module.__name__: + continue + # Get docstrings + docstring = inspect.getdoc(obj) + if docstring: + docs.append(docstring) + return "\n".join(docs) + except ImportError as e: + return f"Error importing module {module_path}: {e}" + + +def unique_component_name(model: pe.ConcreteModel, base: str) -> str: + """ + Brief: Generate a unique component name under the model. + + Operations: + 1) If 'base' exists, append _2, _3, ... until unique + Returns: + str + """ + if not hasattr(model, base): + return base + k = 2 + while hasattr(model, f"{base}_{k}"): + k += 1 + return f"{base}_{k}" + diff --git a/optichat/tools/ldr_explain/__init__.py b/optichat/tools/ldr_explain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/optichat/tools/ldr_explain/core.py b/optichat/tools/ldr_explain/core.py new file mode 100644 index 0000000..9917826 --- /dev/null +++ b/optichat/tools/ldr_explain/core.py @@ -0,0 +1,1122 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, Iterable, Optional, Tuple, Set, Any, List, Sequence, Union + +import json +import pyomo.environ as pyo +from pyomo.core.base.componentuid import ComponentUID + +# NOTE: relative import to work inside the packaged project +from .extractor import LDRExtractor + +__all__ = [ + "LDRPrimalDualCore", + "LDR_solve", + "UncertaintySpec", + "ExtractorOptions", + "BuildOptions", + "SolverOptions", +] + +# ----------------------------- Constants ----------------------------- +CONST_TOKEN: str = "const" + + +# ----------------------------- Module-level helpers (private) ----------------------------- +def _uid(obj: Any) -> str: + """Stable string id for Pyomo Var/Param(Data) via ComponentUID; pass-through if already str.""" + if isinstance(obj, str): + return obj + return str(ComponentUID(obj)) + + +def _all_var_ids(md: Any) -> Set[str]: + """Union of declared var_ids, any vars seen in rows, and any bound names.""" + explicit = set(getattr(md, "var_ids", []) or []) + from_rows = {v for r in md.constraints for v in r.var_coefs.keys()} + from_bounds = set(md.var_bounds.keys()) + return explicit | from_rows | from_bounds + + +def _active_xi_for_row( + row: Any, + var_map: Dict[str, Dict[int, pyo.Var]], + uncertain_params: List[pyo.Param], +) -> Tuple[Set[int], Set[int], Set[int]]: + """ + For a canonical row, compute the active ξ blocks: + Returns (active blocks, var-driven blocks, rhs-driven blocks) as ints. + Block 1 is the intercept; blocks 2.. correspond to uncertain parameters. + """ + var_xi = { + int(xi) + for vk in row.var_coefs + if vk in var_map + for xi in var_map[vk] + if int(xi) > 1 + } + rhs_xi = { + 2 + i + for i, p in enumerate(uncertain_params) + if row.param_coefs.get(_uid(p), 0.0) != 0.0 + } + active = {1} | var_xi | rhs_xi + return active, var_xi, rhs_xi + + +# ===================================================================== +# Friendly option bundles +# ===================================================================== +@dataclass +class UncertaintySpec: + """Minimal uncertainty description.""" + params: Sequence[pyo.Param] # ξ₂…ξ_{k+1} in this order + box: Sequence[Tuple[float, float]] # same length/order as params + + def xi_set(self) -> List[int]: + # We always include intercept block=1; blocks 2..k+1 correspond to params + return [1] + list(range(2, len(self.params) + 2)) + + def bounds(self) -> Sequence[Tuple[float, float]]: + # For BN slacks and bound rows; default to the same box + return list(self.box) + + +@dataclass +class ExtractorOptions: + """How to build variable-ξ reachability.""" + primal_cfg: Optional[Dict[str, Any]] = None + dual_cfg: Optional[Dict[str, Any]] = None + k: int = 0 + khop_temporal: bool = True + + +@dataclass +class BuildOptions: + """How to assemble the LDR models.""" + reduced_primal: bool = False + reduced_dual: bool = False + M_primal: Optional[Dict[Tuple[int, int], float]] = None + M_dual: Optional[Dict[Tuple[int, int], float]] = None + + +@dataclass +class SolverOptions: + name: str = "gurobi" + options: Optional[Dict[str, Any]] = field(default_factory=dict) + tee: bool = False + + +# ===================================================================== +# Public Core +# ===================================================================== +class LDRPrimalDualCore: + """ + Facade for building, solving, and inspecting LDR models for both primal & dual. + + Public APIs: + • build_extract_solve_both(...) # full control (original) + • ldr_expression(...) # print/evaluate learned affine policies + • LDR_solve(...) # simple one-call entrypoint (new) + • solve(...), solve_reduced(...) # optional nice wrappers (new) + """ + + # ---- class-level cache (for ldr_expression) ---- + _last_primal_md = None + _last_dual_md = None + _last_primal_var_map: Dict[str, Dict[int, pyo.Var]] = None + _last_dual_var_map: Dict[str, Dict[int, pyo.Var]] = None + _last_primal_ldr: Optional[pyo.ConcreteModel] = None + _last_dual_ldr: Optional[pyo.ConcreteModel] = None + _last_uncertain_params: Optional[List[pyo.Param]] = None + _last_xi_list: Optional[List[int]] = None + + # ---- instance state for this build ---- + def __init__(self) -> None: + self._primal_md = None + self._dual_md = None + self._primal_var_map: Dict[str, Dict[int, pyo.Var]] = {} + self._dual_var_map: Dict[str, Dict[int, pyo.Var]] = {} + self._primal_ldr: Optional[pyo.ConcreteModel] = None + self._dual_ldr: Optional[pyo.ConcreteModel] = None + self._uncertain_params: List[pyo.Param] = [] + self._xi_list: List[int] = [] + self._bounds: Sequence[Tuple[float, float]] = [] + + # ------------------------------------------------------------------ + # Private helpers + # ------------------------------------------------------------------ + @staticmethod + def _uniform_second_moment_matrix(bounds: Sequence[Tuple[float, float]]) -> Dict[Tuple[int, int], float]: + """ + Build M = E[ ξ̃ ξ̃ᵀ ] for ξ̃ = (1, ξ₁,…,ξ_k) assuming independent Uniform(a_i, b_i). + Blocks are indexed 1..k+1 (1 = intercept). + """ + k = len(bounds) + mu = [0.5 * (a + b) for (a, b) in bounds] + m2 = [(a * a + a * b + b * b) / 3.0 for (a, b) in bounds] + + M: Dict[Tuple[int, int], float] = {} + M[(1, 1)] = 1.0 + for i in range(k): + bi = i + 2 + M[(1, bi)] = mu[i] + M[(bi, 1)] = mu[i] + for j in range(k): + bj = j + 2 + M[(bi, bj)] = m2[i] if i == j else mu[i] * mu[j] + return M + + def _build_affine_vars( + self, + md: Any, + ldr: pyo.ConcreteModel, + xi_set: Iterable[int], # {1,…,k+1}; 1 is intercept + *, + reduced: bool, + var_to_xi: Optional[Dict[Any, Iterable[Any]]] = None, + ) -> Tuple[Dict[str, Dict[int, pyo.Var]], Dict[str, Dict[int, pyo.Var]]]: + """ + Create α-variables for each decision variable in `md`. + + - Continuous vars: α-blocks over xi_set (or pruned by var_to_xi if reduced=True). + - Discrete vars (binary/integer): intercept-only (block 1); no ξ-dependence. + + Returns: + (var_map, readable_map): { var_uid_str : {xi_idx : VarData} } + """ + # normalize mapping tokens (accept CUID/Var/Param or strings) + tok_map: Dict[str, Set[str]] = {} + if reduced and var_to_xi: + for k, seq in var_to_xi.items(): + vk = _uid(k) + tok_map[vk] = {CONST_TOKEN if (t == CONST_TOKEN) else _uid(t) for t in seq} + + # normalize xi indices to ints + xi_list = [int(i) for i in xi_set] + if not xi_list or (1 not in xi_list): + raise ValueError("xi_set must contain 1 (intercept).") + + # map param uid → block index + uid_to_blk = {u: 2 + i for i, u in enumerate(md.uncertain_uids)} + + def tokens_to_blocks(tokens: Set[str]) -> Set[int]: + blocks = set() + if CONST_TOKEN in tokens: + blocks.add(1) + for u in md.uncertain_uids: + if u in tokens: + blocks.add(uid_to_blk[u]) + return blocks + + var_map: Dict[str, Dict[int, pyo.Var]] = {} + readable: Dict[str, Dict[int, pyo.Var]] = {} + + for vk in _all_var_ids(md): + dom = getattr(md, "var_domain", {}).get(vk, "cont") # default to continuous if absent + + # Discrete: intercept-only with proper domain + if dom in ("binary", "integer"): + comp_name = f"ldr_{vk.replace(':','__').replace('[','_').replace(']','_')}" + if dom == "binary": + alpha = pyo.Var([1], domain=pyo.Binary) + else: + alpha = pyo.Var([1], domain=pyo.Integers) + setattr(ldr, comp_name, alpha) + var_map[vk] = {1: alpha[1]} + readable[vk] = var_map[vk] + continue + + # Continuous + if reduced: + toks = tok_map.get(vk, set()) + if not toks: + continue + xi_idx = sorted({1} | tokens_to_blocks(toks)) # keep intercept if kept + else: + xi_idx = list(xi_list) + + if not xi_idx: + continue + + xi_idx = [int(i) for i in xi_idx] + comp_name = f"ldr_{vk.replace(':','__').replace('[','_').replace(']','_')}" + alpha = pyo.Var(xi_idx, domain=pyo.Reals) + setattr(ldr, comp_name, alpha) + var_map[vk] = {int(i): alpha[int(i)] for i in xi_idx} + readable[vk] = var_map[vk] + + return var_map, readable + + def _build_constraints( + self, + md: Any, + ldr: pyo.ConcreteModel, + xi_set: Iterable[int], # {1,…,k+1}; 1 = intercept + var_map: Dict[str, Dict[int, pyo.Var]], + bounds: Sequence[Tuple[float, float]], # [(lb, ub)] for ξ₂ … ξ_{k+1} + uncertain_params: Sequence[pyo.Param], # ξ₂ … ξ_{k+1} in the SAME order + *, + reduced: bool = False, + ) -> None: + """ + Adds: + • robustified row constraints for every row in md.constraints + • robustified deterministic bounds for variables that appear in var_map + + Works for **primal** md and **dual** md identically. + Uses Ben-Tal/Nemirovski linearization with shared slack tensors. + """ + # normalize xi indices to ints + xi_list = [int(i) for i in xi_set] + if 1 not in xi_list: + raise ValueError("xi_set must contain 1 (intercept).") + if len(xi_list) != len(uncertain_params) + 1: + raise ValueError("xi_set length must equal 1 + len(uncertain_params).") + + # ------------------------- (A) Row slacks ------------------------- + ineq_info: List[Tuple[str, List[int]]] = [] + for i, r in enumerate(md.constraints): + if r.sense != "<=": + continue + if reduced and not any(vk in var_map for vk in r.var_coefs): + continue + + key = f"{r.name}_{r.index}_{i}" + active, _, _ = _active_xi_for_row(r, var_map, list(uncertain_params)) + used_pairs = {0, 1} + for u in (active - {1}): + e = 2 * (int(u) - 1) + used_pairs.add(e) + used_pairs.add(e + 1) + ineq_info.append((key, sorted(used_pairs))) + + ineq_keys = [key for key, _ in ineq_info] + ineq_keyset = set(ineq_keys) + + if ineq_keys: + ldr.slack = pyo.Var( + [(key, j) for key, jset in ineq_info for j in jset], + domain=pyo.NonNegativeReals, + ) + ldr.slack_pair = pyo.Constraint( + ineq_keys, rule=lambda m, key: m.slack[key, 0] - m.slack[key, 1] >= 0 + ) + + # ------------------------- (B) Rows themselves -------------------- + for i, row in enumerate(md.constraints): + if reduced and not any(vk in var_map for vk in row.var_coefs): + continue + + active, var_xi, rhs_xi = _active_xi_for_row(row, var_map, list(uncertain_params)) + idx = sorted(active) + cname = f"ldr_row_{i}_{row.name}_{row.index}_{row.sense}" + blk = pyo.Constraint(idx) + setattr(ldr, cname, blk) + + key = f"{row.name}_{row.index}_{i}" + + for b in idx: + # LHS: Σ_j a_ij * α_{j,b} + lhs = 0.0 + for vk, aij in row.var_coefs.items(): + xblock = var_map.get(vk, {}).get(int(b), None) + if xblock is not None and aij != 0.0: + lhs += aij * xblock + + # BN slacks for ≤ rows + if row.sense == "<=" and key in ineq_keyset: + if b == 1: + lhs += ldr.slack[key, 0] - ldr.slack[key, 1] + for u in (var_xi | rhs_xi): + e = 2 * (u - 1) + lb, ub = bounds[u - 2] + lhs += (-lb) * ldr.slack[key, e] + lhs += (ub) * ldr.slack[key, e + 1] + else: + e = 2 * (b - 1) + lhs += ldr.slack[key, e] - ldr.slack[key, e + 1] + + # RHS block + if b == 1: + rhs = row.const + else: + pk = _uid(uncertain_params[b - 2]) + rhs = row.param_coefs.get(pk, 0.0) + + blk[b] = lhs == rhs + + # ------------------------- (C) Bound rows ------------------------- + bound_rows: List[Tuple[str, str, float, float]] = [] + for vk, (lb, ub) in md.var_bounds.items(): + if vk not in var_map: + continue + + # skip binaries entirely — domain already enforces 0/1 + dom = getattr(md, "var_domain", {}).get(vk, "cont") + if dom == "binary": + continue + + if lb != float("-inf"): + bound_rows.append((vk, f"{vk}_lb", -1.0, -lb)) # -x ≤ -lb + if ub != float("+inf"): + bound_rows.append((vk, f"{vk}_ub", +1.0, +ub)) # +x ≤ ub + + if not bound_rows: + return + + bkey_to_pairs: Dict[str, List[int]] = {} + for vk, key, _, _ in bound_rows: + active = {1} | {int(b) for b in var_map[vk] if int(b) > 1} + used_pairs = {0, 1} + for u in (active - {1}): + e = 2 * (u - 1) + used_pairs.add(e) + used_pairs.add(e + 1) + bkey_to_pairs[key] = sorted(used_pairs) + + bkeys = list(bkey_to_pairs.keys()) + ldr.slack_b = pyo.Var( + [(key, j) for key in bkeys for j in bkey_to_pairs[key]], + domain=pyo.NonNegativeReals, + ) + ldr.slack_b_pair = pyo.Constraint( + bkeys, rule=lambda m, key: m.slack_b[key, 0] - m.slack_b[key, 1] >= 0 + ) + + for vk, key, sign, rhs0 in bound_rows: + active = {1} | {int(b) for b in var_map[vk] if int(b) > 1} + idx = sorted(active) + blk = pyo.Constraint(idx) + cname = f"ldr_bound_{key.replace(':','__')}" + setattr(ldr, cname, blk) + + for b in idx: + coeff = sign * var_map[vk].get(int(b), 0) + if b == 1: + lhs = coeff + (ldr.slack_b[key, 0] - ldr.slack_b[key, 1]) + for u in (active - {1}): + e = 2 * (u - 1) + # prefer instance-stored bounds when available (mirrors BN usage above) + lb_u, ub_u = self._bounds[u - 2] if self._bounds else bounds[u - 2] + lhs += (-lb_u) * ldr.slack_b[key, e] + lhs += (ub_u) * ldr.slack_b[key, e + 1] + rhs = rhs0 + else: + e = 2 * (b - 1) + lhs = coeff + ldr.slack_b[key, e] - ldr.slack_b[key, e + 1] + rhs = 0.0 + blk[b] = lhs == rhs + + def _build_objective( + self, + md: Any, + ldr: pyo.ConcreteModel, + xi_set: Iterable[int], # {1,…,k+1}; 1 = intercept + var_map: Dict[str, Dict[int, pyo.Var]], + uncertain_params: Sequence[pyo.Param], # ξ₂ … ξ_{k+1} in same order + *, + M: Optional[Dict[Tuple[int, int], float]] = None, + reduced: bool = False, + ) -> pyo.Objective: + """ + E[ c(ξ)^T x(ξ) ] + E[standalone-ξ terms] + offset, with ξ̃_1 ≡ 1. + If M is None, uses the uniform second-moment matrix implied by md.param_box. + """ + xi_list = [int(i) for i in xi_set] + if 1 not in xi_list: + raise ValueError("xi_set must contain 1 (intercept).") + if len(xi_list) != len(uncertain_params) + 1: + raise ValueError("xi_set length must equal 1 + len(uncertain_params).") + + # Default M: Uniform(a,b) independent + if M is None: + M = self._uniform_second_moment_matrix(md.param_box) + + blk_tok: Dict[int, str] = {1: CONST_TOKEN} + for pos, p in enumerate(uncertain_params, start=2): + blk_tok[pos] = _uid(p) + + def M_(i: int, j: int) -> float: + return float(M[i, j]) + + expr = 0.0 + + # Σ_v Σ_i (Σ_j M_ij C_v[j]) * α_v[i] + for vk in _all_var_ids(md): + if reduced and vk not in var_map: + continue + C = [md.obj_var_coef.get(vk, {}).get(blk_tok[b], 0.0) for b in xi_list] + if all(c == 0.0 for c in C): + continue + MC = [ + sum(M_(bi, bj) * C[jpos] for jpos, bj in enumerate(xi_list)) + for bi in xi_list + ] + for ipos, bi in enumerate(xi_list): + alph = var_map.get(vk, {}).get(int(bi), None) + if alph is not None and MC[ipos] != 0.0: + expr += MC[ipos] * alph + + # stand-alone β_i ξ_i terms → β_i * E[ξ_i] = β_i * M[1, i_block] + for pk, beta in md.obj_param_coef.items(): + for b in xi_list[1:]: + if blk_tok[b] == pk: + expr += beta * M_(1, b) + break + + expr += md.obj_offset + sense = pyo.minimize if (md.obj_sense == pyo.minimize) else pyo.maximize + ldr.obj = pyo.Objective(expr=expr, sense=sense) + return ldr.obj + + @staticmethod + def _solve(model: pyo.ConcreteModel, solver_name: str, solver_options: Optional[Dict[str, Any]], tee: bool) -> Dict[str, Any]: + """ + Thin solver wrapper: identical decision logic for objective extraction/None. + """ + solver = pyo.SolverFactory(solver_name) + if solver is None: + return {"status": "no_solver", "termination": "no_solver", "obj": None, "raw": None} + if solver_options: + for kopt, vopt in solver_options.items(): + solver.options[kopt] = vopt + res = solver.solve(model, tee=tee) + stat = res.solver.status + term = res.solver.termination_condition + + if term in (pyo.TerminationCondition.optimal, pyo.TerminationCondition.locallyOptimal): + try: + val = pyo.value(model.obj) + except Exception: + val = None + elif term in ( + pyo.TerminationCondition.infeasible, + pyo.TerminationCondition.unbounded, + pyo.TerminationCondition.infeasibleOrUnbounded, + pyo.TerminationCondition.maxTimeLimit, + pyo.TerminationCondition.maxIterations, + pyo.TerminationCondition.error, + pyo.TerminationCondition.other, + ): + val = None + else: + try: + val = pyo.value(model.obj) + except Exception: + val = None + + return {"status": str(stat), "termination": str(term), "obj": val, "raw": res} + + # ------------------------------------------------------------------ + # Public API 1: Build both, solve, return results (full control) + # ------------------------------------------------------------------ + @classmethod + def build_extract_solve_both( + cls, + *, + base_model: pyo.ConcreteModel, + uncertain_params: Sequence[pyo.Param], + param_box: Sequence[Tuple[float, float]], + xi_set: Iterable[int], # must contain 1 and have length = 1 + len(uncertain_params) + bounds: Sequence[Tuple[float, float]], # same length/order as uncertain_params + # extractor knobs + primal_cfg: Optional[Dict[str, Any]] = None, + dual_cfg: Optional[Dict[str, Any]] = None, + k: int = 0, + khop_temporal: bool = True, + # LDR building knobs + reduced_primal: bool = False, + reduced_dual: bool = False, + M_primal: Optional[Dict[Tuple[int, int], float]] = None, + M_dual: Optional[Dict[Tuple[int, int], float]] = None, + # solver knobs + solver_name: str = "gurobi", + solver_options: Optional[Dict[str, Any]] = None, + tee: bool = True, + # return extras + return_models: bool = False, + ) -> Dict[str, Any]: + """ + End-to-end helper: + 1) Extract primal & dual ModelData via LDRExtractor + 2) Build LDR variables, constraints, objective for both sides + 3) Solve both (Gurobi by default) + 4) Return objective values and a primal–dual gap (if both numeric) + """ + # create a small instance to hold state and avoid repeating long arg lists + self = cls() + + # --- basic checks on xi_set / bounds --- + xi_list = list(xi_set) + if 1 not in xi_list: + raise ValueError("xi_set must include 1 (the intercept block).") + if len(xi_list) != len(uncertain_params) + 1: + raise ValueError("xi_set length must be 1 + len(uncertain_params).") + if len(bounds) != len(uncertain_params): + raise ValueError("bounds and uncertain_params must have the same length.") + + self._xi_list = [int(i) for i in xi_list] + self._uncertain_params = list(uncertain_params) + self._bounds = bounds + + # --- 1) Extract primal & dual ModelData and incidence maps --- + extractor = LDRExtractor(primal_cfg=primal_cfg or {}, dual_cfg=dual_cfg or {}) + primal_md, primal_map, dual_md, dual_map = extractor.extract_data( + base=base_model, + uncertain_params=uncertain_params, + param_box=param_box, + k=k, + khop_temporal=khop_temporal, + ) + self._primal_md, self._dual_md = primal_md, dual_md + + # --- 2) Build LDR models (Pyomo containers) --- + self._primal_ldr = pyo.ConcreteModel() + self._dual_ldr = pyo.ConcreteModel() + + # α-vars (use reduced mapping iff reduced_* = True) + self._primal_var_map, _ = self._build_affine_vars( + md=self._primal_md, + ldr=self._primal_ldr, + xi_set=self._xi_list, + reduced=reduced_primal, + var_to_xi=(primal_map if reduced_primal else None), + ) + self._dual_var_map, _ = self._build_affine_vars( + md=self._dual_md, + ldr=self._dual_ldr, + xi_set=self._xi_list, + reduced=reduced_dual, + var_to_xi=(dual_map if reduced_dual else None), + ) + + # constraints (robust rows + robust bounds) + self._build_constraints( + md=self._primal_md, + ldr=self._primal_ldr, + xi_set=self._xi_list, + var_map=self._primal_var_map, + bounds=bounds, + uncertain_params=self._uncertain_params, + reduced=reduced_primal, + ) + self._build_constraints( + md=self._dual_md, + ldr=self._dual_ldr, + xi_set=self._xi_list, + var_map=self._dual_var_map, + bounds=bounds, + uncertain_params=self._uncertain_params, + reduced=reduced_dual, + ) + + # objectives (default M = uniform second-moment from md.param_box) + self._build_objective( + md=self._primal_md, + ldr=self._primal_ldr, + xi_set=self._xi_list, + var_map=self._primal_var_map, + uncertain_params=self._uncertain_params, + M=M_primal, + reduced=reduced_primal, + ) + self._build_objective( + md=self._dual_md, + ldr=self._dual_ldr, + xi_set=self._xi_list, + var_map=self._dual_var_map, + uncertain_params=self._uncertain_params, + M=M_dual, + reduced=reduced_dual, + ) + + # --- update class-level cache for ldr_expression() compatibility --- + cls._last_primal_md = self._primal_md + cls._last_dual_md = self._dual_md + cls._last_primal_var_map = self._primal_var_map + cls._last_dual_var_map = self._dual_var_map + cls._last_primal_ldr = self._primal_ldr + cls._last_dual_ldr = self._dual_ldr + cls._last_uncertain_params = self._uncertain_params + cls._last_xi_list = self._xi_list + + # --- 3) Solve both --- + p_res = cls._solve(self._primal_ldr, solver_name, solver_options, tee) + d_res = cls._solve(self._dual_ldr, solver_name, solver_options, tee) + + # gap (primal - dual) only if both are numbers + gap = None + if (p_res["obj"] is not None) and (d_res["obj"] is not None): + gap = float(p_res["obj"]) - float(d_res["obj"]) + + out = { + "primal_obj": p_res["obj"], + "dual_obj": d_res["obj"], + "gap": gap, + "primal_status": {"status": p_res["status"], "termination": p_res["termination"]}, + "dual_status": {"status": d_res["status"], "termination": d_res["termination"]}, + } + if return_models: + out.update({ + "primal_ldr": self._primal_ldr, + "dual_ldr": self._dual_ldr, + }) + return out + + # ------------------------------------------------------------------ + # Public API 2: Expression printer/evaluator + # ------------------------------------------------------------------ + @classmethod + def ldr_expression( + cls, + side: str = "primal", # which side to use for `variables` + variables=None, # Optional[Any | Iterable[Any]] + *, + coeff: str = "values", # "values" | "names" + digits: int = 6, + zero_tol: float = 1e-12, + use_base_names: bool = False, + # Numeric evaluation of LDR at a given ξ vector (order = uncertain_params) + xi_values: Optional[Sequence[float]] = None, # length == len(uncertain_params) + output: str = "expr", # "expr" | "value" | "both" + validate_bounds: bool = True, + # Ask for the **dual** of given primal constraints (in addition to `variables`) + # Accepts: ConstraintData, (name, index|None), or "name" / "name[...]" strings + dual_of: Optional[Union[Any, Iterable[Any]]] = None, + ) -> Dict[str, Union[str, float, Dict[str, Union[str, float]]]]: + """ + Build LDR expressions (as strings) and/or evaluate them at a supplied ξ vector. + Behavior identical to the original implementation. + """ + import pyomo.environ as pyo # local for clarity in helper typing + + # -------- helpers (local, self-contained) ----------------- + def _uid_local(obj) -> str: + if isinstance(obj, str): + return obj + return str(ComponentUID(obj)) + + def _pname(p) -> str: + u = _uid_local(p) + return u.split("[", 1)[0] if use_base_names else u + + def _coef_repr(b: int, var: pyo.Var): + if coeff == "values": + try: + val = pyo.value(var) + if val is None: + return None + if abs(val) <= zero_tol and b != 1: + return None + return round(float(val), digits) + except Exception: + return None + elif coeff == "names": + return str(var) + else: + raise ValueError("coeff must be 'values' or 'names'.") + + def _fmt_index(idx) -> str: + if idx is None: + return "" + tup = idx if isinstance(idx, tuple) else (idx,) + return "[" + ",".join(json.dumps(v) for v in tup) + "]" + + def _normalize_iter(x): + if x is None: + return [] + if isinstance(x, (str, pyo.Var)): + return [x] + try: + return list(x) + except Exception: + return [x] + + def _parse_dual_ref(item) -> Optional[str]: + """ + Convert a primal constraint reference to its dual y uid: + y:{row.name}{_fmt_index(row.index)} + """ + # ConstraintData + if hasattr(item, "parent_component") and hasattr(item, "index"): + try: + name = item.parent_component().name + idx = item.index() # None for scalar + return f"y:{name}{_fmt_index(idx)}" + except Exception: + return None + # (name, index) + if isinstance(item, tuple) and item and isinstance(item[0], str): + name = item[0] + idx = item[1] if len(item) > 1 else None + return f"y:{name}{_fmt_index(idx)}" + # "name" or "name[...]" string + if isinstance(item, str): + if "[" in item and item.endswith("]"): + name, rest = item.split("[", 1) + rest = rest[:-1] # strip trailing ] + toks = [t.strip() for t in rest.split(",")] if rest else [] + try: + idx = tuple(int(t) if t.lstrip("-").isdigit() else t for t in toks) if toks else None + except Exception: + idx = tuple(toks) if toks else None + return f"y:{name}{_fmt_index(idx)}" + else: + return f"y:{item}" + return None + + def _expr_string_for(vk: str, blocks: Dict[int, pyo.Var], blk_label: Dict[int, str]) -> str: + if not blocks: + return "0" + terms: List[str] = [] + # intercept + if 1 in blocks: + c = _coef_repr(1, blocks[1]) + if c is not None: + terms.append(str(c)) + # ξ terms + for b in sorted(k for k in blocks.keys() if k > 1): + c = _coef_repr(b, blocks[b]) + if c is None: + continue + xlbl = blk_label.get(b, f"xi[{b-1}]") + if coeff == "values" and isinstance(c, (int, float)): + if c < 0: + terms.append(f"- {abs(c)}*{xlbl}") + else: + if terms: + terms.append(f"+ {c}*{xlbl}") + else: + terms.append(f"{c}*{xlbl}") + else: + terms.append(f"{c}*{xlbl}") + if not terms: + return "0" + s = " ".join(terms) + if s.startswith("+ "): + s = s[2:] + return s + + def _value_for(blocks: Dict[int, pyo.Var], xi_vals: Sequence[float]) -> float: + """Evaluate α⋅ξ̃ with ξ̃_1=1 and ξ̃_{2+i}=xi_vals[i].""" + total = 0.0 + if 1 in blocks: + a1 = pyo.value(blocks[1]) + if a1 is not None: + total += float(a1) + for i, xi in enumerate(xi_vals, start=2): + if i in blocks: + ai = pyo.value(blocks[i]) + if ai is None: + continue + total += float(ai) * float(xi) + return float(round(total, digits)) + + # -------- pick cached side data ----------------------------------- + side = (side or "").lower() + if side not in ("primal", "dual"): + raise ValueError("side must be 'primal' or 'dual'.") + + if side == "primal": + md = cls._last_primal_md + var_map = cls._last_primal_var_map + else: + md = cls._last_dual_md + var_map = cls._last_dual_var_map + + uncertain_params = cls._last_uncertain_params + xi_blocks = cls._last_xi_list + + if md is None or var_map is None or uncertain_params is None or xi_blocks is None: + raise RuntimeError("No cached LDR build found. Call build_extract_solve_both(...) or LDR_solve(...) first.") + + # -------- set up ξ labels and validate xi_values ------------------- + blk_label: Dict[int, str] = {1: "1"} + for i, p in enumerate(uncertain_params, start=0): + blk_label[2 + i] = _uid(p).split("[", 1)[0] if use_base_names else _uid(p) + + # If user provided xi_values but didn't request 'value', assume they want value. + if xi_values is not None and output == "expr": + output = "value" + + if xi_values is not None: + if len(xi_values) != len(uncertain_params): + raise ValueError( + f"xi_values length {len(xi_values)} must equal number of uncertain params {len(uncertain_params)}." + ) + if validate_bounds and hasattr(md, "param_box") and md.param_box: + for i, (x, (a, b)) in enumerate(zip(xi_values, md.param_box)): + if not (a <= float(x) <= b): + raise ValueError(f"xi_values[{i}]={x} is outside bounds [{a}, {b}].") + + var_items = variables if isinstance(variables, (list, tuple, set)) else ([variables] if variables is not None else []) + dual_items = dual_of if isinstance(dual_of, (list, tuple, set)) else ([dual_of] if dual_of is not None else []) + + # Convert dual_of constraint refs → dual y uids (always from **dual** side) + def _fmt_index(idx) -> str: + if idx is None: + return "" + tup = idx if isinstance(idx, tuple) else (idx,) + return "[" + ",".join(json.dumps(v) for v in tup) + "]" + + def _parse_dual_ref(item) -> Optional[str]: + if hasattr(item, "parent_component") and hasattr(item, "index"): + try: + name = item.parent_component().name + idx = item.index() + return f"y:{name}{_fmt_index(idx)}" + except Exception: + return None + if isinstance(item, tuple) and item and isinstance(item[0], str): + name = item[0] + idx = item[1] if len(item) > 1 else None + return f"y:{name}{_fmt_index(idx)}" + if isinstance(item, str): + if "[" in item and item.endswith("]"): + name, rest = item.split("[", 1) + rest = rest[:-1] + toks = [t.strip() for t in rest.split(",")] if rest else [] + try: + idx = tuple(int(t) if t.lstrip("-").isdigit() else t for t in toks) if toks else None + except Exception: + idx = tuple(toks) if toks else None + return f"y:{name}{_fmt_index(idx)}" + else: + return f"y:{item}" + return None + + dual_y_uids: List[str] = [] + for it in dual_items: + yuid = _parse_dual_ref(it) + if yuid is not None: + dual_y_uids.append(yuid) + + dual_md = cls._last_dual_md + dual_var_map = cls._last_dual_var_map + + # -------- build the answer ---------------------------------------- + result: Dict[str, Union[str, float, Dict[str, Union[str, float]]]] = {} + + # (A) Variables on requested `side` + for v in var_items: + vk = _uid(v) + blocks = var_map.get(vk, {}) + if output == "expr": + result[vk] = _expr_string_for(vk, blocks, blk_label) + elif output == "value": + if xi_values is None: + raise ValueError("Provide xi_values=... when requesting output='value'.") + result[vk] = _value_for(blocks, xi_values) + elif output == "both": + if xi_values is None: + raise ValueError("Provide xi_values=... when requesting output='both'.") + result[vk] = { + "expr": _expr_string_for(vk, blocks, blk_label), + "value": _value_for(blocks, xi_values), + } + else: + raise ValueError("output must be 'expr', 'value', or 'both'.") + + # (B) Dual of primal constraints (always use **dual** side) + for yuid in dual_y_uids: + blocks = (dual_var_map or {}).get(yuid, {}) + if output == "expr": + result[yuid] = _expr_string_for(yuid, blocks, blk_label) + elif output == "value": + if xi_values is None: + raise ValueError("Provide xi_values=... when requesting output='value'.") + result[yuid] = _value_for(blocks, xi_values) + elif output == "both": + if xi_values is None: + raise ValueError("Provide xi_values=... when requesting output='both'.") + result[yuid] = { + "expr": _expr_string_for(yuid, blocks, blk_label), + "value": _value_for(blocks, xi_values), + } + + return result + + # ------------------------------------------------------------------ + # New simple entrypoints + # ------------------------------------------------------------------ + @classmethod + def solve( + cls, + *, + model: pyo.ConcreteModel, + uncertainty: UncertaintySpec, + extractor: ExtractorOptions = ExtractorOptions(), + build: BuildOptions = BuildOptions(), + solver: SolverOptions = SolverOptions(), + return_models: bool = False, + ) -> Dict[str, Any]: + """ + Easiest path: 'full' LDR on both primal and dual (no α-pruning) unless build specifies otherwise. + Users provide model + uncertainty box. Everything else has sane defaults. + """ + xi_set = uncertainty.xi_set() + bounds = uncertainty.bounds() + return cls.build_extract_solve_both( + base_model=model, + uncertain_params=uncertainty.params, + param_box=uncertainty.box, + xi_set=xi_set, + bounds=bounds, + primal_cfg=extractor.primal_cfg, + dual_cfg=extractor.dual_cfg, + k=extractor.k, + khop_temporal=extractor.khop_temporal, + reduced_primal=build.reduced_primal, + reduced_dual=build.reduced_dual, + M_primal=build.M_primal, + M_dual=build.M_dual, + solver_name=solver.name, + solver_options=solver.options, + tee=solver.tee, + return_models=return_models, + ) + + @classmethod + def solve_reduced( + cls, + *, + model: pyo.ConcreteModel, + uncertainty: UncertaintySpec, + extractor: ExtractorOptions = ExtractorOptions(), + solver: SolverOptions = SolverOptions(), + return_models: bool = False, + ) -> Dict[str, Any]: + """ + Same as `solve` but forces α-pruning on both sides. + """ + build = BuildOptions(reduced_primal=True, reduced_dual=True) + return cls.solve( + model=model, + uncertainty=uncertainty, + extractor=extractor, + build=build, + solver=solver, + return_models=return_models, + ) + + # ------------------------------------------------------------------ + # Single "friendly" API name you requested + # ------------------------------------------------------------------ + @classmethod + def LDR_solve( + cls, + *, + model: pyo.ConcreteModel, + uncertainty: Union[ + Tuple[Sequence[pyo.Param], Sequence[Tuple[float, float]]], # (params, box) + UncertaintySpec, + ], + reduced: bool = False, + # extractor knobs + primal_cfg: Optional[Dict[str, Any]] = None, + dual_cfg: Optional[Dict[str, Any]] = None, + k: int = 0, + khop_temporal: bool = True, + # moments + M_primal: Optional[Dict[Tuple[int, int], float]] = None, + M_dual: Optional[Dict[Tuple[int, int], float]] = None, + # solver + solver: str = "gurobi", + solver_options: Optional[Dict[str, Any]] = None, + tee: bool = False, + # extras + return_models: bool = False, + ) -> Dict[str, Any]: + """ + Simple one-call entrypoint. Accepts either: + • uncertainty = (params, box) + • uncertainty = UncertaintySpec(params=..., box=...) + + Example: + res = LDRPrimalDualCore.LDR_solve( + model=m, + uncertainty=([m.xi1, m.xi2], [(0,1), (-2,3)]), + reduced=True, + k=1, + primal_cfg=pcfg, + dual_cfg=dcfg, + solver="gurobi", + solver_options={"Threads": 8}, + ) + """ + # normalize uncertainty + if isinstance(uncertainty, UncertaintySpec): + unc = uncertainty + else: + try: + params, box = uncertainty # type: ignore[misc] + unc = UncertaintySpec(params=params, box=box) + except Exception: + raise TypeError("uncertainty must be (params, box) or an UncertaintySpec instance") + + return cls.solve( + model=model, + uncertainty=unc, + extractor=ExtractorOptions( + primal_cfg=primal_cfg, + dual_cfg=dual_cfg, + k=k, + khop_temporal=khop_temporal, + ), + build=BuildOptions( + reduced_primal=reduced, + reduced_dual=reduced, + M_primal=M_primal, + M_dual=M_dual, + ), + solver=SolverOptions(name=solver, options=solver_options or {}, tee=tee), + return_models=return_models, + ) + + +# ---------------------------------------------------------------------- +# Module-level friendly function name (so users can `from ... import LDR_solve`) +# ---------------------------------------------------------------------- +def LDR_solve( + *, + model: pyo.ConcreteModel, + uncertainty: Union[ + Tuple[Sequence[pyo.Param], Sequence[Tuple[float, float]]], # (params, box) + UncertaintySpec, + ], + reduced: bool = False, + # extractor knobs + primal_cfg: Optional[Dict[str, Any]] = None, + dual_cfg: Optional[Dict[str, Any]] = None, + k: int = 0, + khop_temporal: bool = True, + # moments + M_primal: Optional[Dict[Tuple[int, int], float]] = None, + M_dual: Optional[Dict[Tuple[int, int], float]] = None, + # solver + solver: str = "gurobi", + solver_options: Optional[Dict[str, Any]] = None, + tee: bool = False, + # extras + return_models: bool = False, +) -> Dict[str, Any]: + """ + Top-level thin wrapper so users can call LDR_solve(...) without referencing the class. + """ + return LDRPrimalDualCore.LDR_solve( + model=model, + uncertainty=uncertainty, + reduced=reduced, + primal_cfg=primal_cfg, + dual_cfg=dual_cfg, + k=k, + khop_temporal=khop_temporal, + M_primal=M_primal, + M_dual=M_dual, + solver=solver, + solver_options=solver_options, + tee=tee, + return_models=return_models, + ) diff --git a/optichat/tools/ldr_explain/extractor.py b/optichat/tools/ldr_explain/extractor.py new file mode 100644 index 0000000..9764c9f --- /dev/null +++ b/optichat/tools/ldr_explain/extractor.py @@ -0,0 +1,776 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Dict, List, Sequence, Tuple, Optional, Set, Union, Any, Iterable + +from collections import defaultdict, deque +import json + +import pyomo.environ as pyo +from pyomo.core.base.componentuid import ComponentUID +from pyomo.core.base.param import Param, ParamData +from pyomo.core.base.var import VarData +from pyomo.core.expr.visitor import identify_variables, replace_expressions +from pyomo.repn.linear import LinearRepnVisitor +from pyomo.core.expr.numeric_expr import ( + NegationExpression, + SumExpression, + ProductExpression, + LinearExpression, +) + +# ============================ Constants & Type Aliases ============================ +CONST_TOKEN: str = "const" +INF: float = float("+inf") +NINF: float = float("-inf") + +UIDStr = str +Bounds = Tuple[float, float] +CoefMap = Dict[UIDStr, float] +VarXiBlocks = Dict[UIDStr, Dict[UIDStr, float]] + + +# ----------------------------- dataclasses (string IDs) ----------------------------- +@dataclass(frozen=True) +class ConstraintRow: + name: str + index: Optional[Tuple] + sense: str # '==' or '<=' + const: float # RHS-style constant + var_coefs: Dict[UIDStr, float] # keys: var uid strings + param_coefs: Dict[UIDStr, float] # keys: param uid strings + + +@dataclass(frozen=True) +class ModelData: + # (1) Variables and bounds (string IDs) + var_ids: List[UIDStr] # can be empty for primal/dual, but we now fill dual too + var_bounds: Dict[UIDStr, Bounds] # uid -> (lb, ub) + var_domain: Dict[UIDStr, str] # uid -> {"cont","binary","integer"} + + # (2) Canonical rows + constraints: List[ConstraintRow] + + # (3) Objective + obj_sense: int + obj_var_coef: VarXiBlocks # var uid -> {CONST_TOKEN or ξ uid -> coef} + obj_param_coef: Dict[UIDStr, float] # stand-alone ξ in objective (rare) + obj_offset: float + + # (4) Uncertainty + uncertain_uids: List[UIDStr] + param_box: Sequence[Bounds] + + +# ============================ string UID helpers ============================ +def uid(obj: Any) -> UIDStr: + """Stable string id for Pyomo Var/Param(Data) via ComponentUID; pass-through if already str.""" + if isinstance(obj, str): + return obj + return str(ComponentUID(obj)) + + +def base_name(uid_str: UIDStr) -> str: + return uid_str.split("[", 1)[0] + + +def split_name_and_index(uid_str: UIDStr) -> Tuple[str, Tuple]: + if "[" not in uid_str: + return uid_str, () + name, tail = uid_str.split("[", 1) + tail = tail.rstrip("]") + parts = [p.strip() for p in tail.split(",")] if tail else [] + + def cast(z: str) -> Union[int, str]: + try: + return int(z) + except ValueError: + return z + + return name, tuple(cast(p) for p in parts) + + +def time_of(uid_str: UIDStr, tpos: Optional[int]) -> Optional[Any]: + if tpos is None: + return None + _, idx = split_name_and_index(uid_str) + return idx[tpos] if tpos < len(idx) else None + + +def fmt_index(idx: Optional[Union[Tuple, Any]]) -> str: + if idx is None: + return "" + tup = idx if isinstance(idx, tuple) else (idx,) + return "[" + ",".join(json.dumps(v) for v in tup) + "]" + + +def all_var_uids(md: ModelData) -> Set[UIDStr]: + """Union of declared var_ids, any vars seen in rows, and any bound-dual names.""" + explicit = set(md.var_ids) + from_rows = {v for r in md.constraints for v in r.var_coefs.keys()} + from_bounds = set(md.var_bounds.keys()) + return explicit | from_rows | from_bounds + +def _domain_tag(v: VarData) -> str: + """ + Domain tagging (binary vs. cont) using modern Pyomo APIs. + + - 'binary' if VarData.is_binary() is available and returns True + - 'cont' otherwise (includes integers and continuous) + + If the expected modern helpers are not present, raise a clear error suggesting + a more recent Pyomo version, rather than guessing from legacy domain internals. + """ + # Check for modern VarData helpers + has_is_binary = callable(getattr(v, "is_binary", None)) + has_is_integer = callable(getattr(v, "is_integer", None)) # not used for tag, but signals modern API + has_is_continuous = callable(getattr(v, "is_continuous", None)) # not used for tag, but signals modern API + + if not (has_is_binary or has_is_integer or has_is_continuous): + raise RuntimeError( + "Cannot determine variable domain using modern Pyomo APIs " + "(VarData.is_binary / is_integer / is_continuous are not available). " + "Please use a more recent Pyomo version." + ) + + # Only binaries get the 'binary' tag + if has_is_binary and v.is_binary(): + return "binary" + + # Everything else (including integers) is treated as continuous + return "cont" + + +# ============================ extractor class (string-based) ============================ +class LDRExtractor: + """ + String-UID based extractor for primal/dual linearized representations with uncertainty. + Public API: + - extract_primal_md + - build_dual_md + - build_var_xi_map + - extract_data + """ + + def __init__(self, primal_cfg: Optional[Dict[str, Any]] = None, dual_cfg: Optional[Dict[str, Any]] = None): + self.primal_cfg: Dict[str, Any] = primal_cfg or {} + # Dual config defaults to primal config unless explicitly provided + self.dual_cfg: Dict[str, Any] = dual_cfg if dual_cfg is not None else self.primal_cfg + + # ---------- objective split (returns string-keyed dicts) ---------- + def _extract_obj(self, expr: Any, uncertain_params_flat: Sequence[ParamData]) -> Tuple[VarXiBlocks, Dict[UIDStr, float], float]: + """ + Decompose objective expression into: + - var_param_coef: per-variable blocks keyed by CONST_TOKEN or ξ uid + - obj_param_coef: stand-alone uncertain parameter terms + - offset: constant + Logic preserved; only imports consolidated and minor style cleanups. + """ + var_param_coef: VarXiBlocks = {} + obj_param_coef: Dict[UIDStr, float] = {} + offset = 0.0 + uids = {uid(p) for p in uncertain_params_flat} + + def rec(e: Any, mult: float = 1.0) -> float: + # numbers or any Pyomo-constant expression + if isinstance(e, (int, float)): + return float(e) * mult + if hasattr(e, "is_constant") and e.is_constant(): + return float(pyo.value(e)) * mult + + # single variable + if isinstance(e, VarData): + k = uid(e) + bucket = var_param_coef.setdefault(k, {}) + bucket[CONST_TOKEN] = bucket.get(CONST_TOKEN, 0.0) + mult + return 0.0 + + # single parameter (maybe uncertain) + if isinstance(e, ParamData): + k = uid(e) + if k in uids: + obj_param_coef[k] = obj_param_coef.get(k, 0.0) + mult + else: + return float(e.value) * mult + return 0.0 + + # unary minus + if isinstance(e, NegationExpression): + return rec(e.arg(0), -mult) + + # sum + if isinstance(e, SumExpression): + s = 0.0 + for a in e.args: + s += rec(a, mult) + return s + + # product + if isinstance(e, ProductExpression): + # flatten product factors + fs: List[Any] = [] + + def flat(p: Any) -> None: + if isinstance(p, ProductExpression): + for a in p.args: + flat(a) + else: + fs.append(p) + + flat(e) + + coef = mult + nvar = npar = 0 + vobj: Optional[VarData] = None + pobj: Optional[ParamData] = None + lin_term: Optional[Any] = None # one Linear/Sum factor allowed + + for f in fs: + if isinstance(f, (int, float)): + coef *= float(f) + elif hasattr(f, "is_constant") and f.is_constant(): + coef *= float(pyo.value(f)) + elif isinstance(f, VarData): + nvar += 1 + vobj = f + elif isinstance(f, ParamData): + k = uid(f) + if k in uids: + npar += 1 + pobj = f + else: + coef *= float(f.value) + elif isinstance(f, (SumExpression, LinearExpression)): + if lin_term is None: + lin_term = f + else: + raise NotImplementedError("Product of two linear/sum terms is not supported.") + else: + raise NotImplementedError(f"Unsupported factor {type(f)}") + + # Distribute over a single linear/sum factor + if lin_term is not None: + # Real bilinear patterns (linear * variable) are not allowed + if nvar >= 1: + raise NotImplementedError("Bilinear term (linear * variable) is not supported.") + + # Build linear repn of the linear factor + repn = LinearRepnVisitor({}).walk_expression(lin_term) + id_to_var = {id(v): v for v in identify_variables(lin_term)} + + # Case A: one uncertain param times linear → expand to sum of (param*var) + (param*const) + if npar == 1 and pobj is not None: + pk = uid(pobj) + # constant part * param → stand-alone uncertain term in objective + cst = coef * float(repn.constant) + if cst != 0.0: + obj_param_coef[pk] = obj_param_coef.get(pk, 0.0) + cst + # variable parts → (param * var) coefficients + for vid, vcoef in repn.linear.items(): + v = id_to_var.get(vid, None) + if v is None: + continue + vk = uid(v) + bucket = var_param_coef.setdefault(vk, {}) + bucket[pk] = bucket.get(pk, 0.0) + coef * float(vcoef) + return 0.0 + + # Case B: no uncertain param (just constants * linear) → distribute + if npar == 0: + total = coef * float(repn.constant) + for vid, vcoef in repn.linear.items(): + v = id_to_var.get(vid, None) + if v is None: + continue + total += rec(v, coef * float(vcoef)) + return total + + # Otherwise (e.g., multiple uncertain params) → not supported + raise NotImplementedError("Product with >1 uncertain parameter is not supported.") + + # No linear factor: handle simple cases + if nvar > 1 or npar > 1: + raise NotImplementedError("Nonlinear term (var*var or ξ*ξ) is not supported.") + if nvar == 1 and npar == 1: + vk = uid(vobj) # type: ignore[arg-type] + pk = uid(pobj) # type: ignore[arg-type] + bucket = var_param_coef.setdefault(vk, {}) + bucket[pk] = bucket.get(pk, 0.0) + coef + return 0.0 + if nvar == 1: + vk = uid(vobj) # type: ignore[arg-type] + bucket = var_param_coef.setdefault(vk, {}) + bucket[CONST_TOKEN] = bucket.get(CONST_TOKEN, 0.0) + coef + return 0.0 + if npar == 1: + pk = uid(pobj) # type: ignore[arg-type] + obj_param_coef[pk] = obj_param_coef.get(pk, 0.0) + coef + return 0.0 + + # only constants remain + return coef + + # unknown node + raise NotImplementedError(f"Unknown expr node {type(e)}") + + offset += rec(expr) + return var_param_coef, obj_param_coef, offset + + # ---------- primal extraction → string ModelData ---------- + def extract_primal_md( + self, + base: pyo.ConcreteModel, + uncertain_params: Sequence[Union[Param, ParamData]], + param_box: Sequence[Bounds], + ) -> ModelData: + """ + Build a string-keyed ModelData from a concrete Pyomo model. + NOTE: Relies on a module-level `_domain_tag(v: VarData) -> str` helper + to classify variables as 'cont' | 'integer' | 'binary'. + """ + # -------- (0) Flatten uncertain params in the user-provided order -------- + flat: List[ParamData] = [] + for p in uncertain_params: + if isinstance(p, Param): + flat.extend(list(p.values())) + elif isinstance(p, ParamData): + flat.append(p) + else: + flat.append(p) # allow already-flat inputs + + if len(flat) != len(param_box): + raise ValueError("param_box length must match uncertain_params length.") + + # -------- (1) Variables, IDs, bounds, domains -------- + var_datas = list(base.component_data_objects(pyo.Var, descend_into=True)) + + # Deterministic IDs for reproducibility + var_ids = sorted(uid(v) for v in var_datas) + + # Bounds map: uid → (lb, ub) with ±inf where absent + var_bounds: Dict[UIDStr, Bounds] = { + uid(v): (v.lb if v.has_lb() else NINF, v.ub if v.has_ub() else INF) + for v in var_datas + } + + # Domains via module-level helper `_domain_tag` + var_domain: Dict[UIDStr, str] = {uid(v): _domain_tag(v) for v in var_datas} + + # -------- (2) Constraints → canonical rows using dummy ξ substitution -------- + rows: List[ConstraintRow] = [] + tmp = pyo.Var(range(len(flat)), initialize=0.0) + base.add_component("_scratch_tmp_params", tmp) + try: + sub_map = {id(p): tmp[i] for i, p in enumerate(flat)} + dummy_ids = {id(dv) for dv in tmp.values()} + + for block in base.component_objects(pyo.Constraint, active=True, descend_into=True): + for idx in block: + c = block[idx] + body = c.body + if c.equality: + sense, lhs, rhs, sign = "==", body, c.lower, +1 + elif c.has_lb() and not c.has_ub(): + sense, lhs, rhs, sign = "<=", body, c.lower, -1 # ≥ becomes ≤ after flip + elif c.has_ub() and not c.has_lb(): + sense, lhs, rhs, sign = "<=", body, c.upper, +1 + else: + raise ValueError("Two-sided range constraints not supported.") + + # Shift to LHS with optional sign flip for ≥ + expr_shift = sign * (lhs - rhs) + # Substitute uncertain params with temporary Vars to read linear repn + expr_sub = replace_expressions(expr_shift, sub_map) + repn_sub = LinearRepnVisitor({}).walk_expression(expr_sub) + + # Collect variable coefficients (skip dummy ξ vars) + var_coefs: CoefMap = {} + for v in identify_variables(expr_sub): + if id(v) in dummy_ids: + continue + coef = repn_sub.linear.get(id(v), 0.0) + if coef: + var_coefs[uid(v)] = float(coef) + + # Collect ξ coefficients by reading the dummy coefficients and negating + param_coefs: CoefMap = {} + for i, p in enumerate(flat): + coef = -repn_sub.linear.get(id(tmp[i]), 0.0) + if coef: + param_coefs[uid(p)] = float(coef) + + # Constant term (RHS-style): -repn.constant + const = -float(repn_sub.constant) + + rows.append( + ConstraintRow( + name=block.name, + index=idx if block.is_indexed() else None, + sense=sense, + const=const, + var_coefs=var_coefs, + param_coefs=param_coefs, + ) + ) + finally: + base.del_component("_scratch_tmp_params") + + # -------- (3) Objective decomposition -------- + if not hasattr(base, "obj"): + raise RuntimeError("Model must contain an active objective named 'obj'.") + + obj_var_coef, obj_param_coef, obj_offset = self._extract_obj(base.obj.expr, flat) + + # -------- (4) Assemble ModelData -------- + return ModelData( + var_ids=var_ids, + var_bounds=var_bounds, + constraints=rows, + obj_sense=base.obj.sense, + obj_var_coef=obj_var_coef, + obj_param_coef=obj_param_coef, + obj_offset=obj_offset, + uncertain_uids=[uid(p) for p in flat], + param_box=list(param_box), + var_domain=var_domain, + ) + + # ---------- dual MD from primal MD (all strings) ---------- + def build_dual_md(self, primal: ModelData) -> ModelData: + """ + Build the dual ModelData from a primal ModelData. Preserves all original logic. + """ + is_primal_max = (primal.obj_sense == pyo.maximize) + dual_sense = pyo.minimize if is_primal_max else pyo.maximize + + def row_dual_bounds(sense: str) -> Bounds: + if sense == "==": + return (NINF, INF) + # primal row is '<=' + return (0.0, INF) if is_primal_max else (NINF, 0.0) + + def bound_dual_bounds() -> Bounds: + return (0.0, INF) if is_primal_max else (NINF, 0.0) + + var_bounds_dual: Dict[UIDStr, Bounds] = {} + obj_var_coef_dual: VarXiBlocks = {} + dual_var_ids: List[UIDStr] = [] + + # helper for naming row duals + def yname(row: ConstraintRow) -> str: + return f'y:{row.name}{fmt_index(row.index)}' + + # 1) Row dual variables (always present) + for r in primal.constraints: + yn = yname(r) + dual_var_ids.append(yn) + var_bounds_dual[yn] = row_dual_bounds(r.sense) + block: Dict[UIDStr, float] = {CONST_TOKEN: float(r.const)} + for puid, coef in r.param_coefs.items(): + block[puid] = block.get(puid, 0.0) + float(coef) + obj_var_coef_dual[yn] = block + + # helpers for bound-dual names + def ylb(vk: UIDStr) -> str: + return f"y_lb:{vk}" + + def yub(vk: UIDStr) -> str: + return f"y_ub:{vk}" + + # 2) Dual constraints (one per primal variable/column) + # First gather A^T coefficients from row-duals + rows_by_var: Dict[UIDStr, List[Tuple[str, float]]] = {} + for r in primal.constraints: + yn = yname(r) + for vk, aij in r.var_coefs.items(): + rows_by_var.setdefault(vk, []).append((yn, float(aij))) + + dual_rows: List[ConstraintRow] = [] + + for vk, (lb, ub) in primal.var_bounds.items(): + # LHS from row duals only (we'll add nonzero bound-duals if needed) + lhs: CoefMap = {} + for yn, aij in rows_by_var.get(vk, []): + lhs[yn] = lhs.get(yn, 0.0) + aij + + # Detect bound type + has_nonzero_lb = (lb != NINF) and (lb != 0.0) + has_nonzero_ub = (ub != INF) and (ub != 0.0) + is_free = (lb == NINF) and (ub == INF) + is_nonneg = (lb == 0.0) and (ub == INF) + is_nonpos = (ub == 0.0) and (lb == NINF) + + # Add bound-dual columns ONLY for nonzero finite bounds + if has_nonzero_ub: + yn_ub = yub(vk) + dual_var_ids.append(yn_ub) + var_bounds_dual[yn_ub] = bound_dual_bounds() + obj_var_coef_dual[yn_ub] = {CONST_TOKEN: float(ub)} + lhs[yn_ub] = lhs.get(yn_ub, 0.0) + 1.0 + + if has_nonzero_lb: + yn_lb = ylb(vk) + dual_var_ids.append(yn_lb) + var_bounds_dual[yn_lb] = bound_dual_bounds() + obj_var_coef_dual[yn_lb] = {CONST_TOKEN: float(-lb)} + lhs[yn_lb] = lhs.get(yn_lb, 0.0) - 1.0 + + # RHS blocks from c_j(ξ) = c^0 + Σ C_{j,ξ} ξ + blocks = primal.obj_var_coef.get(vk, {}) + const = float(blocks.get(CONST_TOKEN, 0.0)) + rhs_xi = {pk: float(c) for pk, c in blocks.items() if pk != CONST_TOKEN} + + # Decide the sense and possibly flip signs to keep '<=' canonicalization + if has_nonzero_lb or has_nonzero_ub or is_free: + # Equality if we used any nonzero finite bound-dual, or var is free + sense = "==" + else: + # Zero-bound cases: encode inequality by variable sign + # For primal MAX: + # x ≥ 0 → Aᵀy ≥ c (write as -Aᵀy ≤ -c) + # x ≤ 0 → Aᵀy ≤ c (already ≤) + # For primal MIN: directions flip. + if is_nonneg: + if is_primal_max: + lhs = {k: -v for k, v in lhs.items()} + const = -const + rhs_xi = {k: -v for k, v in rhs_xi.items()} + sense = "<=" + elif is_nonpos: + if not is_primal_max: + lhs = {k: -v for k, v in lhs.items()} + const = -const + rhs_xi = {k: -v for k, v in rhs_xi.items()} + sense = "<=" + else: + # Fallback to equality (should not occur given the above cases) + sense = "==" + + dual_rows.append( + ConstraintRow( + name="dual_col", + index=(vk,), + sense=sense, + const=const, + var_coefs=lhs, + param_coefs=rhs_xi, + ) + ) + + dual_var_domain = {vk: "cont" for vk in dual_var_ids} + + return ModelData( + var_ids=dual_var_ids, # only the variables we actually created + var_bounds=var_bounds_dual, + constraints=dual_rows, + obj_sense=dual_sense, + obj_var_coef=obj_var_coef_dual, + obj_param_coef={}, # dual has no stand-alone ξ terms + obj_offset=0.0, + uncertain_uids=list(primal.uncertain_uids), + param_box=list(primal.param_box), + var_domain=dual_var_domain, + ) + + # ---------- unified ξ-map for either primal or dual (string-based) ---------- + def build_var_xi_map( + self, + md: ModelData, + *, + manual_map: Optional[Dict[Any, Iterable[Any]]] = None, + cfg: Optional[Dict[str, Any]] = None, + k: int = 0, + khop_temporal: bool = False, + ) -> Dict[UIDStr, Set[UIDStr]]: + """ + Mapping logic (works the same for primal & dual): + + - No cfg → every variable → all uncertain ξ. + - With cfg: + - Only ξ whose base name appears in cfg["parameters"] are considered. + - Temporal ξ (parameters[p]["t_pos"] is int): + * If khop_temporal=False: window by the variable's own time index using variables[v]["t_pos"] and "window". + * If khop_temporal=True: also use k-hop reachability (same as non-temporal). + - Non-temporal ξ (parameters[p]["t_pos"] is None): k-hop reachability. + Seeding for k-hop uses BOTH row param_coefs AND md.obj_var_coef[var] (important for the dual). + """ + cfg = self.primal_cfg if cfg is None else cfg + + # Manual override + if manual_map: + all_xi = set(md.uncertain_uids) + out_manual: Dict[UIDStr, Set[UIDStr]] = {} + for v_key, plist in manual_map.items(): + vk = v_key if isinstance(v_key, str) else uid(v_key) + if isinstance(plist, str) and plist == "__ALL__": + out_manual[vk] = set(all_xi) + continue + sel: Set[UIDStr] = set() + for p in plist: + pk = p if isinstance(p, str) else uid(p) + if pk in all_xi: + sel.add(pk) + out_manual[vk] = sel + return out_manual + + # No cfg → every var → all ξ + if not cfg: + xi = set(md.uncertain_uids) + return {v: set(xi) for v in all_var_uids(md)} + + vcfg = cfg.get("variables", {}) + pcfg = cfg.get("parameters", {}) + + # Helpers that respect base names like "y:inv_bal" + def v_tpos(vname: str) -> Optional[int]: + return vcfg.get(vname, {}).get("t_pos", None) + + def p_tpos(pname: str) -> Optional[int]: + return pcfg.get(pname, {}).get("t_pos", None) + + def v_window(vname: str) -> Tuple[Optional[int], Optional[int]]: + w = vcfg.get(vname, {}).get("window", {}) + return w.get("past", None), w.get("future", None) + + # ξ universe when cfg is present: ONLY those listed in cfg["parameters"] + pcfg_names = set(pcfg.keys()) # base names + permitted_xi = {p for p in md.uncertain_uids if base_name(p) in pcfg_names} + + # Classify ξ by temporal/non-temporal via cfg["parameters"][...]["t_pos"] + temporal_xi = {p for p in permitted_xi if p_tpos(base_name(p)) is not None} + non_temporal_xi = permitted_xi - temporal_xi + + # Rows & graph (used for k-hop) + row_vars = [list(r.var_coefs.keys()) for r in md.constraints] + row_params = [list(r.param_coefs.keys()) for r in md.constraints] + + # Build variable adjacency from constraints + var_adj: Dict[UIDStr, Set[UIDStr]] = defaultdict(set) + for vs in row_vars: + for i, v1 in enumerate(vs): + for v2 in vs[i + 1 :]: + var_adj[v1].add(v2) + var_adj[v2].add(v1) + + # Direct seeding for k-hop: + # - from rows: param_coefs on the same row as the variable + # - from objective: md.obj_var_coef[var] (important for dual) + var_to_ti_direct: Dict[UIDStr, Set[UIDStr]] = defaultdict(set) + var_to_tt_direct: Dict[UIDStr, Set[UIDStr]] = defaultdict(set) + + # From rows + for rid, vs in enumerate(row_vars): + ps = row_params[rid] + for v in vs: + for p in ps: + if p in permitted_xi: + if p in non_temporal_xi: + var_to_ti_direct[v].add(p) + else: + var_to_tt_direct[v].add(p) + + # From objective (per-var ξ costs) + for v, blocks in md.obj_var_coef.items(): + for pk, coef in blocks.items(): + if pk == CONST_TOKEN: + continue + if pk in permitted_xi: + if pk in non_temporal_xi: + var_to_ti_direct[v].add(pk) + else: + var_to_tt_direct[v].add(pk) + + # k-hop collector + def khop_collect(start_v: UIDStr, k_: int, direct: Dict[UIDStr, Set[UIDStr]]) -> Set[UIDStr]: + if k_ <= 0: + return set(direct[start_v]) + seen = {start_v} + q = deque([(start_v, 0)]) + got: Set[UIDStr] = set() + while q: + v, d = q.popleft() + got |= direct[v] + if d == k_: + continue + for nb in var_adj[v]: + if nb not in seen: + seen.add(nb) + q.append((nb, d + 1)) + return got + + # Compute mapping + out: Dict[UIDStr, Set[UIDStr]] = {} + all_vs = list(all_var_uids(md)) # include y_lb:/y_ub: etc. + + for v in all_vs: + vb = base_name(v) + + # Non-temporal via k-hop + ti = khop_collect(v, k, var_to_ti_direct) + + # Temporal: windowing by default; optionally k-hop if requested + if khop_temporal: + tt = khop_collect(v, k, var_to_tt_direct) + else: + tt = set() + vtpos = v_tpos(vb) + if vtpos is not None: + tv = time_of(v, vtpos) + past, future = v_window(vb) + for p in temporal_xi: + ptpos = p_tpos(base_name(p)) + if ptpos is None: + continue + tp = time_of(p, ptpos) + if tp is None or tv is None: + continue + if (past is not None and tp < tv - past) or (future is not None and tp > tv + future): + continue + tt.add(p) + # if var has no t_pos in cfg, it simply doesn't get temporal ξ by windowing + + out[v] = (ti | tt) + + # Final filter (should be no-op because we used permitted_xi) + allowed = set(md.uncertain_uids) + for v in out: + out[v] = {p for p in out[v] if p in allowed and p in permitted_xi} + + return out + + # ---------- end-to-end (returns both sides) ---------- + def extract_data( + self, + base: pyo.ConcreteModel, + uncertain_params: Sequence[Union[Param, ParamData]], + param_box: Sequence[Bounds], + *, + primal_manual_map: Optional[Dict[Any, Iterable[Any]]] = None, + dual_manual_map: Optional[Dict[Any, Iterable[Any]]] = None, + k: int = 1, + khop_temporal: bool = False, + ) -> Tuple[ModelData, Dict[UIDStr, Set[UIDStr]], ModelData, Dict[UIDStr, Set[UIDStr]]]: + """ + Run full pipeline: + - extract primal ModelData + - build dual ModelData + - build var→ξ maps for both (with optional separate configs) + """ + primal_md = self.extract_primal_md(base, uncertain_params, param_box) + dual_md = self.build_dual_md(primal_md) + + primal_map = self.build_var_xi_map( + primal_md, + manual_map=primal_manual_map, + cfg=self.primal_cfg, + k=k, + khop_temporal=khop_temporal, + ) + dual_map = self.build_var_xi_map( + dual_md, + manual_map=dual_manual_map, + cfg=self.dual_cfg, # you can pass a different config for dual here + k=k, + khop_temporal=khop_temporal, + ) + return primal_md, primal_map, dual_md, dual_map diff --git a/optichat/tools/python_repl.py b/optichat/tools/python_repl.py new file mode 100644 index 0000000..4a20042 --- /dev/null +++ b/optichat/tools/python_repl.py @@ -0,0 +1,47 @@ +from typing import List, Dict, Any +from typing import Optional +from loguru import logger +from langchain_experimental.utilities import PythonREPL +# from langchain_core.tools import Tool +from google.adk.tools import ToolContext +from optichat.config.constants import * + +python_repl = PythonREPL() +# python_repl_tool = Tool( +# name="python_repl", +# description="A Python shell. Use this to execute python commands. Input should be a valid python snippet", +# func=python_repl.run, +# ) + + +def python_repl_func(code_snippet: str, tool_context: ToolContext) -> Dict[str, str]: + """ + Execute Python code and return the result. + ONLY interact with in code_snippet. + + Args: + code_snippet (str): Python code to execute + + Returns: + Dict[str, str]: a single key named "result" and its value as the execution result + """ + import optichat.tools.shortcut_functions as shortcut_functions + for name in dir(shortcut_functions): + item = getattr(shortcut_functions, name) + if callable(item) and not name.startswith("_"): + python_repl.globals[name] = item + logger.info(f"Injected shortcut function: {name} into REPL") + models_dictionary = tool_context.state[MODELS_DICTIONARY].copy() + python_repl.globals[MODELS_DICTIONARY.lower()] = models_dictionary + logger.info(f"Injected models_dictionary into REPL") + # execute the code snippet + logger.info(f"Python code to execute:\n{code_snippet}") + result = str(python_repl.run(code_snippet)) + # update MODELS_DICTIONARY if a model was modified and solved in REPL + tool_context.state[MODELS_DICTIONARY] = models_dictionary + tool_context.state[MODEL_VERSIONS] = list(models_dictionary.keys()) + return {"result": result} + + + + diff --git a/optichat/tools/rag_tool.py b/optichat/tools/rag_tool.py new file mode 100644 index 0000000..15eaaab --- /dev/null +++ b/optichat/tools/rag_tool.py @@ -0,0 +1,149 @@ +from loguru import logger +from typing import Dict, Any, List +from typing import Optional +from google.genai import types +from google.adk.agents.callback_context import CallbackContext +from google.adk.tools.tool_context import ToolContext +from google.adk.tools.base_tool import BaseTool + +from langchain_chroma import Chroma +from langchain_openai import OpenAIEmbeddings +from langchain_community.document_loaders import PyPDFLoader +from langchain_community.document_loaders.text import TextLoader +from langchain_community.document_loaders.generic import GenericLoader +from langchain_community.document_loaders.parsers import LanguageParser +from langchain_text_splitters import Language, RecursiveCharacterTextSplitter +from openai import embeddings +from optichat.config.constants import CFG +from optichat.config.rag_cfg import * + + +def init_code_rag(path: str, model_name: str): + """ +path -> docs -> update vector store collection +Recommended way to load source code: +https://docs.langchain.com/oss/python/integrations/document_loaders/source_code +The parser can be disabled for small files. +This approach needs path to be a folder (or a single file) and uses glob pattern to load files. +TODO: Not sure if there exists an alternative way to load files from a list of specific files. + """ + code_loader = GenericLoader.from_filesystem( + path, + glob="*", + suffixes=[".py"], + parser=LanguageParser(language=Language.PYTHON, parser_threshold=CODE_RAG_PARSER_THRESHOLD)) + docs = code_loader.load() + + if CODE_RAG_IS_SPLITTED: + code_splitter = RecursiveCharacterTextSplitter.from_language( + language=Language.PYTHON, chunk_size=CODE_RAG_CHUNK_SIZE, chunk_overlap=CODE_RAG_CHUNK_OVERLAP) + docs = code_splitter.split_documents(docs) + + collection_name = f"{model_name}_code" + init_chroma_collection(collection_name, docs, empty_existing=True) + + +def init_paper_rag(paths: List[str], model_name: str): + """ + path -> docs -> update vector store collection + """ + docs = [] + for path in paths: + if path.endswith(".pdf"): + pdf_loader = PyPDFLoader(path) + docs.extend(pdf_loader.load()) + elif path.endswith(".txt"): + txt_loader = TextLoader(path) + docs.extend(txt_loader.load()) + else: + raise ValueError(f"File type of {path} not supported.") + + if PAPER_RAG_IS_SPLITTED: + paper_splitter = RecursiveCharacterTextSplitter( + chunk_size=PAPER_RAG_CHUNK_SIZE, chunk_overlap=PAPER_RAG_CHUNK_OVERLAP, add_start_index=True) + docs = paper_splitter.split_documents(docs) + + collection_name = f"{model_name}_paper" + init_chroma_collection(collection_name, docs, empty_existing=True) + + +def init_chroma_collection(collection_name, docs, empty_existing): + embeddings = OpenAIEmbeddings(model=EMBEDDING_MODEL) + persist_directory = PERSIST_DIRECTORY + if empty_existing: + # TODO: for development only, delete the collection first and then add documents to re-build the collection again + vector_store = get_chroma_vs(collection_name, embeddings, persist_directory) + logger.debug(f"Deleting collection {[col.name for col in vector_store._client.list_collections()]} first for re-building.") + vector_store._client.delete_collection(name=collection_name) + logger.debug(f"After deletion, existing collections are {[col.name for col in vector_store._client.list_collections()]}") + vector_store = get_chroma_vs(collection_name, embeddings, persist_directory) + logger.debug(f"Building collection {collection_name} by adding documents...") + vector_store.add_documents(documents=docs) + logger.debug(f"After adding documents, existing collections are {[col.name for col in vector_store._client.list_collections()]}") + + +def get_chroma_vs(collection_name, embeddings, persist_directory): + vector_store = Chroma( + collection_name=collection_name, + embedding_function=embeddings, + persist_directory=persist_directory) + return vector_store + + +def convert_docs_to_str(result_docs): + result_str = f"Retrieved {len(result_docs)} documents: " + for i, doc in enumerate(result_docs): + result_str += f"\nSource {i+1}: {doc.metadata.get('source', 'unknown source')} \nContent {i+1}: {doc.page_content}\n" + return result_str + + +def code_rag(request: str, tool_context: ToolContext) -> str: + """ + code_rag retrieves code blocks from by + performing semantic similarity search against the submitted request. + + Args: + request: the information that you want to get from , be specific and detailed + + Returns: + string containing the code blocks that are relevant to the request submitted by you. + """ + cfg = tool_context.state[CFG] + if "models_code" in cfg: + model_name = cfg["model_name"] + collection_name = f"{model_name}_code" + vector_store = get_chroma_vs(collection_name=collection_name, + embeddings=OpenAIEmbeddings(model=EMBEDDING_MODEL), + persist_directory=PERSIST_DIRECTORY) + retriever = vector_store.as_retriever(search_type=CODE_RAG_SEARCH_TYPE, + search_kwargs=CODE_RAG_SEARCH_KWARGS) + result = convert_docs_to_str(retriever.invoke(request)) + return {"result": result} + else: + return {"result": "No 'models_code' in cfg, cannot use code_rag."} + + +def paper_rag(request: str, tool_context: ToolContext) -> str: + """ + paper_rag retrieves paper contents from by + performing semantic similarity search against the submitted request. + + Args: + request: the information that you want to get from , be specific and detailed + + Returns: + string containing the paper contents that are relevant to the request submitted by you. + """ + cfg = tool_context.state[CFG] + if "models_paper" in cfg: + model_name = cfg["model_name"] + collection_name = f"{model_name}_paper" + vector_store = get_chroma_vs(collection_name=collection_name, + embeddings=OpenAIEmbeddings(model=EMBEDDING_MODEL), + persist_directory=PERSIST_DIRECTORY) + retriever = vector_store.as_retriever(search_type=PAPER_RAG_SEARCH_TYPE, + search_kwargs=PAPER_RAG_SEARCH_KWARGS) + result = convert_docs_to_str(retriever.invoke(request)) + return {"result": result} + else: + return {"result": "No 'models_paper' in cfg, cannot use paper_rag."} \ No newline at end of file diff --git a/optichat/tools/robust_analysis/__init__.py b/optichat/tools/robust_analysis/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/optichat/tools/robust_analysis/robustness_analysis.py b/optichat/tools/robust_analysis/robustness_analysis.py new file mode 100644 index 0000000..780a9f4 --- /dev/null +++ b/optichat/tools/robust_analysis/robustness_analysis.py @@ -0,0 +1,540 @@ +""" +One-call robustness analysis on a Pyomo model with Option-B param handling. + +What this does +-------------- +1) You provide a zero-arg model factory that returns: + (model, uncertain_params, bounds, xi_set) + where `uncertain_params` may contain scalar or indexed Param/ParamData, and + `bounds` align 1:1 with those entries (see bounds formats below). + +2) We generate `n_scenarios` samples for EACH scalar/indexed entry using your + scenario generator. Internally we "flatten for sampling" but keep the public + API in Option B style. The scenario table uses canonical column names: + - scalar: name + - 1D index: name[i] + - multi-idx: name[i,j,...] + +3) Solve the model ONCE at baseline. Snapshot variable values. + +4) For each scenario: + - Set Param/ParamData values from the scenario row (scalar & indexed supported) + - Reapply stored variable values (no re-solve) + - Evaluate objective and constraint feasibility + +5) Save: + - Results -> `out_path` + - Scenarios-> `out_path + ".scenarios.csv"` + +Bounds formats (per `uncertain_params` entry) +--------------------------------------------- +If the entry is: +- ParamData (scalar): bounds must be (lb, ub) +- scalar Param component: bounds must be (lb, ub) +- indexed Param: one of: + * single (lb, ub) -> broadcast to all indices + * dict {index: (lb,ub)} -> per-index mapping + * sequence aligned to `param.keys()` order, each (lb,ub) + +Distribution +------------ +- `dist="uniform"` or `"normal"` (Gaussian CLIPPED to [lb, ub]). +- If you need per-parameter distributions/means/stds, extend as needed. + +Notes +----- +- We DO NOT re-optimize per scenario; we evaluate the fixed baseline solution. +- To also flag var bound/domain violations after parameter changes, add a check pass. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any, Callable, Dict, Mapping, Optional, Sequence, Tuple, Union, List + +import numpy as np +import pandas as pd +import pyomo.environ as pyo +from pyomo.core.base.param import Param, ParamData +from pyomo.core.base.var import VarData +from pyomo.core.base.constraint import Constraint +from pyomo.core.base.componentuid import ComponentUID +ParamLike = Union["Param", "ParamData"] + +# Use your existing generator +from optichat.tools.robust_analysis.scenario_generator import generate_scenarios_from_model + +__all__ = ["run_robustness"] + +def _collect_var_values(model: pyo.ConcreteModel) -> Dict[str, Dict[Any, float]]: + """ + Snapshot current values for all active Var components. + + Returns + ------- + Dict[str, Dict[Any, float]] + Mapping: -> { index_or_None : value } + """ + out: Dict[str, Dict[Any, float]] = {} + for vcomp in model.component_objects(pyo.Var, active=True, descend_into=True): + is_indexed = vcomp.is_indexed() + vals: Dict[Any, float] = {} + for v in vcomp.values(): + if not isinstance(v, VarData): + continue + val = pyo.value(v, exception=False) + if val is None: + val = 0.0 + key = v.index() if is_indexed else None + vals[key] = float(val) + out[vcomp.name] = vals + return out + + +def _solve_once( + model: pyo.ConcreteModel, + solver: str = "gurobi", + solver_options: Optional[Mapping[str, Any]] = None, +) -> Dict[str, Dict[Any, float]]: + """ + Solve the model once and return a snapshot of variable values. + + - Works for LP/MILP, any mix of continuous/binary/integer vars. + - If the requested solver isn't available, falls back to: cbc → glpk → highs. + + Raises + ------ + RuntimeError + If no supported LP/MIP solver is available. + """ + sf = pyo.SolverFactory(solver) + if not sf.available(): + for cand in ("cbc", "glpk", "highs"): + sf = pyo.SolverFactory(cand) + if sf.available(): + break + else: + raise RuntimeError(f"No LP/MIP solver available among: {solver}, cbc, glpk, highs") + + if solver_options: + for k, v in solver_options.items(): + sf.options[k] = v + + sf.solve(model, tee=False, load_solutions=True) + return _collect_var_values(model) + + +def _check_constraint_feasible(c: Constraint, tol: float) -> bool: + """ + Check feasibility of a single ConstraintData with tolerance. + + Returns + ------- + bool + True iff (lb - tol) <= body <= (ub + tol). + """ + body = pyo.value(c.body, exception=False) + lb = pyo.value(c.lower, exception=False) if c.has_lb() else None + ub = pyo.value(c.upper, exception=False) if c.has_ub() else None + ok_lb = True if lb is None else (body >= lb - tol) + ok_ub = True if ub is None else (body <= ub + tol) + return bool(ok_lb and ok_ub) + + + +# --------------------------------------------------------------------------- # +# Helpers # +# --------------------------------------------------------------------------- # + +def _comp_name(obj: Any) -> str: + """Stable-ish component name (falls back to ComponentUID).""" + try: + return obj.parent_component().name # type: ignore[attr-defined] + except Exception: + return str(ComponentUID(obj)) + + +def _idx_to_str(idx: Any) -> str: + if isinstance(idx, tuple): + return ",".join(map(str, idx)) + return str(idx) + + +def _param_indexed_col_name(pname: str, idx: Any) -> str: + """Canonical Option-B column name: name[i] or name[i,j].""" + return f"{pname}[{_idx_to_str(idx)}]" + + +def _save_df(df: pd.DataFrame, path: Union[str, Path]) -> Path: + out = Path(path) + out.parent.mkdir(parents=True, exist_ok=True) + suf = out.suffix.lower() + if suf == ".csv": + df.to_csv(out, index=False) + elif suf == ".json": + df.to_json(out, orient="records", indent=2) + elif suf == ".parquet": + df.to_parquet(out, index=False) + else: + raise ValueError("Output must end with .csv, .json, or .parquet") + return out + + +def _collect_var_values(model: pyo.ConcreteModel) -> Dict[str, Dict[Any, float]]: + """ + Snapshot current Var values: {VarName: {index_or_None: value}}. + """ + out: Dict[str, Dict[Any, float]] = {} + for vcomp in model.component_objects(pyo.Var, active=True, descend_into=True): + is_indexed = vcomp.is_indexed() + vals: Dict[Any, float] = {} + for v in vcomp.values(): + if not isinstance(v, VarData): + continue + val = pyo.value(v, exception=False) + if val is None: + val = 0.0 + key = v.index() if is_indexed else None + vals[key] = float(val) + out[vcomp.name] = vals + return out + + +def _apply_var_values(model: pyo.ConcreteModel, var_values: Mapping[str, Mapping[Any, float]]) -> None: + """ + Restore Var values from snapshot (no re-solve). + """ + for vcomp in model.component_objects(pyo.Var, active=True, descend_into=True): + mapping = var_values.get(vcomp.name) + if not mapping: + continue + is_indexed = vcomp.is_indexed() + for v in vcomp.values(): + key = v.index() if is_indexed else None + if key not in mapping: + continue + val = mapping[key] + try: + v.set_value(val) + except TypeError: + try: + v.set_value(val, skip_validation=True) + except TypeError: + v.value = val + + +def _evaluate_objective(model: pyo.ConcreteModel) -> Optional[float]: + """ + Evaluate the first active Objective expression; None if absent. + """ + try: + if hasattr(model, "obj"): + return float(pyo.value(model.obj.expr)) + for obj in model.component_data_objects(pyo.Objective, active=True, descend_into=True): + return float(pyo.value(obj.expr)) + except Exception: + pass + return None + + +def _constraint_key(c: Constraint) -> str: + """ + Readable, stable key for a ConstraintData (e.g., myCon[i,j]). + """ + comp = c.parent_component() + base = comp.name + if comp.is_indexed(): + idx = c.index() + return _param_indexed_col_name(base, idx) + return base + + +# ---------- Option-B: setter that supports scalar & indexed Params ---------- # + +def _set_params_from_row( + uncertain_params: Sequence[Union[Param, ParamData]], + row: pd.Series, +) -> None: + for p in uncertain_params: + if isinstance(p, ParamData): + # primary expected column (bare name for scalar ParamData) + bare = _comp_name(p) + # secondary: bracketed fallback, in case a generator produced it that way + try: + idx = p.index() + except Exception: + idx = None + bracketed = None if (idx is None or (isinstance(idx, tuple) and len(idx) == 0)) else \ + _param_indexed_col_name(p.parent_component().name, idx) + + col = None + if bare in row.index: + col = bare + elif bracketed and bracketed in row.index: + col = bracketed + + if col is None: + tried = [bare] + ([bracketed] if bracketed else []) + raise KeyError(f"Scenario missing parameter column for ParamData; tried {tried}.") + + p.set_value(float(row[col])) + continue + + if isinstance(p, Param): + pname = p.parent_component().name if hasattr(p, "parent_component") else p.name + if p.is_indexed(): + for k in p.keys(): + col = _param_indexed_col_name(pname, k) + if col not in row.index: + raise KeyError(f"Scenario missing parameter column '{col}'.") + p[k].set_value(float(row[col])) + else: + pdata = next(iter(p.values())) + default_col = _comp_name(pdata) + col = default_col if default_col in row.index else pname + if col not in row.index: + raise KeyError( + f"Scenario missing parameter column for scalar Param '{pname}'. " + f"Tried '{default_col}' and '{pname}'." + ) + try: + p.set_value(float(row[col])) + except Exception: + p.value = float(row[col]) + continue + + raise TypeError(f"Unsupported param type for '{_comp_name(p)}': {type(p)}") + + +# --------- Internal: make generator inputs & reconcile column names ---------- # + +def _expand_params_for_sampling( + uncertain_params: Sequence[Union[Param, ParamData]], + bounds: Sequence[Union[Tuple[float, float], Dict[Any, Tuple[float, float]], Sequence[Tuple[float, float]]]], +) -> Tuple[List[ParamData], List[Tuple[float, float]], List[str], List[str]]: + """ + Build a FLAT list of ParamData and aligned bounds for sampling. + + Returns + ------- + flat_params : list[ParamData] + flat_bounds : list[(lb, ub)] + desired_names : list[str] + Canonical Option-B names we want in the scenario table (name or name[i], name[i,j]). + generator_base_names : list[str] + Names the scenario generator will initially produce (component names) + BEFORE its duplicate disambiguation. We use these to predict the + generator's unique column names and build a rename map. + + Notes + ----- + The scenario generator (by default) infers names from parent component names, + so multiple ParamData from the same component collide and are fixed by adding + suffixes like '__2'. We precompute that disambiguation to (a) pass correct + per-param distribution settings and (b) rename back to Option-B names. + """ + if len(uncertain_params) != len(bounds): + raise ValueError("`uncertain_params` and `bounds` must have the same length.") + + flat_params: List[ParamData] = [] + flat_bounds: List[Tuple[float, float]] = [] + desired_names: List[str] = [] + generator_base_names: List[str] = [] + + for param_entry, b in zip(uncertain_params, bounds): + if isinstance(param_entry, ParamData): + if not (isinstance(b, tuple) and len(b) == 2): + raise ValueError("Bounds for ParamData must be a single (lb, ub).") + flat_params.append(param_entry) + flat_bounds.append((float(b[0]), float(b[1]))) + + pd_comp = param_entry.parent_component() + comp_name = pd_comp.name + # >> FIX: only use bracket form if index is not None/empty + try: + idx = param_entry.index() + except Exception: + idx = None + if idx is None or (isinstance(idx, tuple) and len(idx) == 0): + desired_names.append(comp_name) # e.g., 'rhs_eq' + else: + desired_names.append(_param_indexed_col_name(comp_name, idx)) # e.g., 'rhs[i]' + generator_base_names.append(comp_name) + continue + + if isinstance(param_entry, Param): + if param_entry.is_indexed(): + keys = list(param_entry.keys()) + # normalize bounds b + if isinstance(b, tuple) and len(b) == 2: + b_list = [b] * len(keys) + elif isinstance(b, dict): + b_list = [b[k] for k in keys] + elif isinstance(b, (list, tuple)) and len(b) == len(keys) and all( + isinstance(x, tuple) and len(x) == 2 for x in b + ): + b_list = list(b) + else: + raise ValueError( + f"Bounds for indexed Param '{param_entry.name}' must be " + f"(lb, ub), or dict{{index:(lb,ub)}}, or a sequence aligned to keys()." + ) + + for k, (lb, ub) in zip(keys, b_list): + pd = param_entry[k] + flat_params.append(pd) + flat_bounds.append((float(lb), float(ub))) + desired_names.append(_param_indexed_col_name(param_entry.name, k)) + generator_base_names.append(param_entry.name) + else: + # scalar Param component + if not (isinstance(b, tuple) and len(b) == 2): + raise ValueError(f"Bounds for scalar Param '{param_entry.name}' must be a single (lb, ub).") + pd = next(iter(param_entry.values())) + flat_params.append(pd) + flat_bounds.append((float(b[0]), float(b[1]))) + desired_names.append(param_entry.name) + generator_base_names.append(param_entry.name) + continue + + raise TypeError(f"Unsupported uncertain param type: {type(param_entry)}") + + return flat_params, flat_bounds, desired_names, generator_base_names + + +def _disambiguate_like_generator(basenames: List[str]) -> List[str]: + """ + Reproduce the scenario generator's uniqueness policy: + if duplicates, suffix subsequent occurrences with '__k' (1-based k>1). + """ + seen: Dict[str, int] = {} + out: List[str] = [] + for nm in basenames: + c = seen.get(nm, 0) + out.append(nm if c == 0 else f"{nm}__{c+1}") + seen[nm] = c + 1 + return out + + +# --------------------------------------------------------------------------- # +# Public: One-call API # +# --------------------------------------------------------------------------- # + +def run_robustness( + *, + model: pyo.ConcreteModel, + uncertain_params: Sequence[ParamLike], + bounds: List, + n_scenarios: int, + dist: str = "uniform", + seed: Optional[int] = None, + out_path: Optional[str] = "robust_results.csv", + tol: float = 1e-6, + solver: str = "gurobi", +) -> pd.DataFrame: + """ + Solve once, evaluate across sampled scenarios (Option B: scalar & indexed Params). + + Parameters + ---------- + model : Callable[[], (model, uncertain_params, bounds, xi_set)] + Zero-argument factory. + n_scenarios : int + Number of scenarios to sample. + dist : {"uniform", "normal"}, default "uniform" + Global distribution for ALL entries (normal is clipped to [lb, ub]). + seed : int | None, default None + RNG seed. + out_path : str, default "robust_results.csv" + Results path (.csv | .json | .parquet). Scenarios saved next to it as + `out_path + ".scenarios.csv"`. + tol : float, default 1e-6 + Constraint feasibility tolerance. + solver : str, default "gurobi" + Primary solver; fallbacks: cbc → glpk → highs. + + Returns + ------- + pandas.DataFrame + Columns: + scenario_id, + , + objective, + , , ... + """ + if n_scenarios <= 0: + raise ValueError("n_scenarios must be positive.") + + dist = (dist or "uniform").strip().lower() + if dist not in ("uniform", "normal"): + raise ValueError("dist must be 'uniform' or 'normal'.") + + # 1) Build model & unpack uncertainty + model_obj = model # noqa: F841 + + # 2) Build FLAT ParamData + bounds for sampling, and compute both: + # - desired Option-B names (name[index]) for final table + # - generator unique names, to pass per-param dist and later rename + flat_params, flat_bounds, desired_names, gen_basenames = _expand_params_for_sampling( + uncertain_params, bounds + ) + gen_unames = _disambiguate_like_generator(gen_basenames) + + # per-param dist mapping uses the generator's unique names + per_param_dist = {nm: dist for nm in gen_unames} + + # 3) Generate scenarios using your generator + scenarios = generate_scenarios_from_model( + uncertain_params=flat_params, # ParamData only + bounds=flat_bounds, # 1:1 with flat_params + n=n_scenarios, + seed=seed, + per_param_dist=per_param_dist, # drive uniform/normal + per_param_normal=None, # defaults if normal + ) + + # The generator's column order is ["scenario_id"] + gen_unames. + # Build a rename map to canonical Option-B names. + rename_map = {old: new for old, new in zip([c for c in scenarios.columns if c != "scenario_id"], desired_names)} + scenarios = scenarios.rename(columns=rename_map) + + # Save the exact scenarios used (with Option-B column names) + scenarios_out = Path(out_path).with_suffix(Path(out_path).suffix + ".scenarios.csv") + scenarios.to_csv(scenarios_out, index=False) + + # 4) Solve once at baseline + stored_vars = _solve_once(model_obj, solver=solver) + + # 5) Constraints roster + con_list: List[Constraint] = list( + model_obj.component_data_objects(pyo.Constraint, active=True, descend_into=True) + ) + con_cols: List[str] = [_constraint_key(c) for c in con_list] + + # 6) Evaluate stored solution per scenario (no re-solve) + param_cols = [c for c in scenarios.columns if c != "scenario_id"] + rows: List[Dict[str, Any]] = [] + for _, srow in scenarios.iterrows(): + # Set params from Option-B columns + _set_params_from_row(uncertain_params, srow) + _apply_var_values(model_obj, stored_vars) + obj_val = _evaluate_objective(model_obj) + + out_row: Dict[str, Any] = {"scenario_id": int(srow["scenario_id"])} + # Echo all param columns (already Option-B names) + for nm in param_cols: + out_row[nm] = float(srow[nm]) if nm != "scenario_id" else int(srow[nm]) + out_row["objective"] = obj_val + # Constraint feasibility flags + for c, cname in zip(con_list, con_cols): + out_row[cname] = 0 if _check_constraint_feasible(c, tol=tol) else 1 + + rows.append(out_row) + + results = pd.DataFrame(rows) + + # 7) Persist & return + _save_df(results, out_path) + print(f"[OK] Wrote {len(scenarios)} scenarios -> {scenarios_out.name}") + print(f"[OK] Wrote results -> {Path(out_path).name} (shape {results.shape[0]} x {results.shape[1]})") + return results diff --git a/optichat/tools/robust_analysis/scenario_generator.py b/optichat/tools/robust_analysis/scenario_generator.py new file mode 100644 index 0000000..6c97e76 --- /dev/null +++ b/optichat/tools/robust_analysis/scenario_generator.py @@ -0,0 +1,466 @@ +#!/usr/bin/env python3 +# scenario_generator.py +""" +Scenario generator tailored to a Pyomo model factory function. + +Expected model function signature (as in your example): + model, uncertain_params, bounds, xi_set = simple_lp_model() + +- `uncertain_params`: list[ParamData or Param] (scalars or arrays; here: scalars) +- `bounds`: list[tuple[lb, ub]] in the SAME ORDER as `uncertain_params` + +Features +-------- +- Uniform or Normal (clipped) sampling per parameter. +- Deterministic seeding for reproducibility. +- Clean DataFrame output with stable parameter names. +- Optional save to CSV / JSON / Parquet + sidecar .meta.json. +- CLI wrapper with dynamic model import. + +Quick Start (Library) +--------------------- +from scenario_generator import generate_scenarios_from_model, save_scenarios +from your_module import simple_lp_model + +model, uncertain_params, bounds, xi_set = simple_lp_model() + +df = generate_scenarios_from_model( + uncertain_params=uncertain_params, + bounds=bounds, + n=1000, + seed=42, + per_param_dist={"rhs_eq": "uniform", "rhs_ge": "normal", "rhs_le": "uniform"}, + per_param_normal={"rhs_ge": {"mean": 20.0, "std": 5.0}}, # optional +) + +save_scenarios(df, "scenarios.csv", meta={"model": "simple_lp_model"}) + +Quick Start (CLI) +----------------- +python scenario_generator.py \ + --n 1000 \ + --seed 42 \ + --out scenarios.csv \ + --model "your_module:simple_lp_model" \ + --dist '{"rhs_ge":"normal"}' \ + --normal '{"rhs_ge":{"mean":20,"std":5}}' + +Notes +----- +- "Normal" sampling here is clipped to [lb, ub]. This is NOT a truncated normal. +- Names for parameters are inferred from Pyomo components and disambiguated if needed. +""" + +from __future__ import annotations + +import argparse +import importlib +import json +from pathlib import Path +from typing import Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union + +import numpy as np +import pandas as pd + +try: + import pyomo.environ as pyo # noqa: F401 (import check) + from pyomo.core.base.param import Param, ParamData + from pyomo.core.base.componentuid import ComponentUID +except Exception as e: + raise RuntimeError("Pyomo is required for scenario_generator.py") from e + + +# ----------------------------- Public Interface ----------------------------- # + +__all__ = [ + "generate_scenarios_from_model", + "save_scenarios", + "main", +] + + +# ----------------------------- Internal Helpers ----------------------------- # + +ParamLike = Union["Param", "ParamData"] +Bounds = Sequence[Tuple[float, float]] + + +def _uid_name(obj: Union[ParamLike, str]) -> str: + """Return a stable-ish identifier for a Pyomo Param/ParamData or pass through a string.""" + if isinstance(obj, str): + return obj + return str(ComponentUID(obj)) + + +def _infer_param_names(uncertain_params: Sequence[ParamLike]) -> List[str]: + """ + Infer column names for each uncertainty object. + + Strategy: + - Prefer the parent component's `name` (common for scalar Params). + - Fall back to a ComponentUID string when needed. + - Ensure uniqueness by suffixing duplicates with `__{k}`. + """ + names: List[str] = [] + for p in uncertain_params: + try: + nm = p.parent_component().name # type: ignore[attr-defined] + except Exception: + nm = _uid_name(p) + names.append(nm) + + if len(names) != len(set(names)): + seen: Dict[str, int] = {} + uniq: List[str] = [] + for nm in names: + c = seen.get(nm, 0) + uniq.append(nm if c == 0 else f"{nm}__{c+1}") + seen[nm] = c + 1 + names = uniq + + return names + + +def _validate_dimensions(uncertain_params: Sequence[ParamLike], bounds: Bounds, n: int) -> None: + """Validate lengths and bounds format.""" + if n <= 0: + raise ValueError("n must be a positive integer.") + + if len(uncertain_params) != len(bounds): + raise ValueError( + f"`uncertain_params` and `bounds` must have the same length " + f"(got {len(uncertain_params)} vs {len(bounds)})." + ) + + for i, (lb, ub) in enumerate(bounds): + if not (isinstance(lb, (int, float)) and isinstance(ub, (int, float))): + raise ValueError(f"bounds[{i}] must be numeric (got {lb}, {ub}).") + if lb >= ub: + raise ValueError(f"bounds[{i}] requires lb < ub (got lb={lb}, ub={ub}).") + + +def _validate_per_param_keys(names: Sequence[str], per_param_dist: Dict[str, str], per_param_normal: Dict[str, Dict[str, float]]) -> None: + """ + Ensure user-provided per-parameter configs only reference known names. + Fails fast on any typo to avoid silent misconfiguration. + """ + valid = set(names) + bad_dist = [k for k in per_param_dist.keys() if k not in valid] + bad_norm = [k for k in per_param_normal.keys() if k not in valid] + if bad_dist: + raise KeyError(f"Unknown parameter names in `per_param_dist`: {bad_dist}. Valid: {sorted(valid)}") + if bad_norm: + raise KeyError(f"Unknown parameter names in `per_param_normal`: {bad_norm}. Valid: {sorted(valid)}") + + +def _sample_uniform(lb: float, ub: float, n: int, rng: np.random.Generator) -> np.ndarray: + """Uniform sampling on [lb, ub].""" + return rng.uniform(lb, ub, size=n) + + +def _sample_normal_clipped(lb: float, ub: float, mean: float, std: float, n: int, rng: np.random.Generator) -> np.ndarray: + """ + Sample from Normal(mean, std) and clip to [lb, ub]. + + NOTE: Clipping != truncated normal. For robustness analyses where the box + is normative and tails are not critical, this is commonly acceptable. + """ + x = rng.normal(loc=mean, scale=std, size=n) + return np.clip(x, lb, ub) + + +# ----------------------------- Core API ----------------------------- # + +def generate_scenarios_from_model( + *, + uncertain_params: Sequence[ParamLike], + bounds: Bounds, + n: int, + seed: Optional[int] = None, + per_param_dist: Optional[Dict[str, str]] = None, + per_param_normal: Optional[Dict[str, Dict[str, float]]] = None, +) -> pd.DataFrame: + """ + Generate i.i.d. scenarios for a given list of uncertain Pyomo params and their box bounds. + + Workflow (at a glance) + ---------------------- + 1) Infer stable parameter names from the provided Pyomo Params/ParamData. + 2) For each parameter j with bounds [lb_j, ub_j], draw n samples via either: + - Uniform(lb_j, ub_j), or + - Normal(mean_j, std_j) then clip to [lb_j, ub_j]. + 3) Return an (n x (1+d)) DataFrame with a leading integer `scenario_id` column. + + Parameters + ---------- + uncertain_params : Sequence[Param | ParamData] + The uncertain scalars in the SAME ORDER you pass their bounds. + bounds : Sequence[Tuple[float, float]] + Box bounds [(lb_1, ub_1), ..., (lb_d, ub_d)] matching `uncertain_params`. + n : int + Number of scenarios to draw. + seed : Optional[int], default None + RNG seed for reproducibility. Uses numpy Generator. + per_param_dist : Optional[Dict[str, str]], default None + Optional mapping {param_name: "uniform" | "normal"}. + Defaults to "uniform" for any name not listed. + Names must match the inferred column names (see return DataFrame). + per_param_normal : Optional[Dict[str, Dict[str, float]]], default None + Optional mapping of normal hyperparameters per parameter: + {"param_name": {"mean": float, "std": float}} + Missing keys fall back to: + mean = (lb+ub)/2 + std = (ub-lb)/6 # ~99.7% of mass within [lb, ub] pre-clipping + + Returns + ------- + pandas.DataFrame + Columns: ["scenario_id", "", ..., ""]. + + Raises + ------ + ValueError + - If lengths mismatch or bounds invalid. + - If `n <= 0` or a provided `std <= 0`. + KeyError + - If `per_param_dist` / `per_param_normal` reference unknown parameter names. + + Example + ------- + >>> # df = generate_scenarios_from_model( + ... # uncertain_params=[...], + ... # bounds=[(0, 10), (5, 25)], + ... # n=1000, + ... # seed=123, + ... # per_param_dist={"rhs_ge": "normal"}, + ... # per_param_normal={"rhs_ge": {"mean": 20.0, "std": 4.0}}, + ... # ) + """ + _validate_dimensions(uncertain_params, bounds, n) + + names = _infer_param_names(uncertain_params) + rng = np.random.default_rng(seed) + + per_param_dist = (per_param_dist or {}).copy() + per_param_normal = (per_param_normal or {}).copy() + _validate_per_param_keys(names, per_param_dist, per_param_normal) + + d = len(uncertain_params) + out = np.empty((n, d), dtype=float) + + for j, nm in enumerate(names): + lb, ub = bounds[j] + dist = (per_param_dist.get(nm, "uniform") or "uniform").strip().lower() + if dist not in ("uniform", "normal"): + raise ValueError(f"Invalid dist for '{nm}': {dist!r} (expected 'uniform' or 'normal').") + + if dist == "uniform": + out[:, j] = _sample_uniform(lb, ub, n, rng) + else: + spec = per_param_normal.get(nm, {}) + mean = float(spec.get("mean", 0.5 * (lb + ub))) + std = float(spec.get("std", (ub - lb) / 6.0)) + if std <= 0: + raise ValueError(f"Normal std must be positive for '{nm}' (got {std}).") + out[:, j] = _sample_normal_clipped(lb, ub, mean, std, n, rng) + + df = pd.DataFrame(out, columns=names) + df.insert(0, "scenario_id", np.arange(1, n + 1, dtype=int)) + return df + + +def save_scenarios( + df: pd.DataFrame, + out_path: Union[str, Path], + *, + meta: Optional[Dict[str, object]] = None, +) -> Path: + """ + Save the scenario table to CSV/JSON/Parquet and a sidecar `.meta.json`. + + The sidecar captures basic provenance such as shape, column names, and any + user-provided metadata. + + Parameters + ---------- + df : pandas.DataFrame + Output from `generate_scenarios_from_model(...)`. + out_path : str | pathlib.Path + A path ending in one of: .csv | .json | .parquet + meta : dict, optional + Arbitrary metadata to include (e.g., model name, seed, config dicts). + + Returns + ------- + pathlib.Path + The resolved output file path. + + Raises + ------ + ValueError + If the output suffix is not one of the supported formats. + """ + out = Path(out_path) + out.parent.mkdir(parents=True, exist_ok=True) + + suf = out.suffix.lower() + if suf not in (".csv", ".json", ".parquet"): + raise ValueError("Output must be one of: .csv, .json, .parquet") + + if suf == ".csv": + df.to_csv(out, index=False) + elif suf == ".json": + df.to_json(out, orient="records", indent=2) + else: + df.to_parquet(out, index=False) + + payload = { + "file": str(out.resolve()), + "n": int(df.shape[0]), + "d": int(df.shape[1] - 1), # exclude scenario_id + "columns": [c for c in df.columns if c != "scenario_id"], + "note": "Normal sampling is clipped to [lb, ub] (not truncated).", + } + if meta: + payload["extra"] = meta + + meta_path = out.with_suffix(out.suffix + ".meta.json") + with meta_path.open("w", encoding="utf-8") as f: + json.dump(payload, f, indent=2) + + return out + + +# ----------------------------- CLI Utilities ----------------------------- # + +def _load_model_factory(model_spec: str) -> Callable[[], Tuple[object, List[ParamLike], Bounds, object]]: + """ + Load a model factory function from a spec like 'package.module:function'. + + The function is expected to return: (model, uncertain_params, bounds, xi_set). + + Parameters + ---------- + model_spec : str + Import path in the form 'pkg.mod:function'. If None-like, falls back + to 'your_module:simple_lp_model' for backward compatibility. + + Returns + ------- + Callable + Zero-arg callable that builds and returns (model, uncertain_params, bounds, xi_set). + + Raises + ------ + ValueError + If the spec is malformed or the attribute is missing. + """ + spec = (model_spec or "your_module:simple_lp_model").strip() + if ":" not in spec: + raise ValueError( + f"Invalid --model spec {spec!r}. Expected 'package.module:function'." + ) + mod_name, func_name = spec.split(":", 1) + try: + mod = importlib.import_module(mod_name) + except Exception as e: + raise ValueError(f"Could not import module {mod_name!r}: {e}") from e + try: + func = getattr(mod, func_name) + except AttributeError as e: + raise ValueError(f"Module {mod_name!r} has no attribute {func_name!r}.") from e + if not callable(func): + raise ValueError(f"{spec!r} did not resolve to a callable.") + return func # type: ignore[return-value] + + +def _parse_args(argv: Optional[Iterable[str]] = None) -> argparse.Namespace: + """ + Parse CLI arguments. + + Important: + - `--model` lets you point to any factory function via 'pkg.module:function'. + Defaults to 'your_module:simple_lp_model' (the original example). + - `--dist` / `--normal` expect JSON strings. + + Examples: + --------- + --dist '{"rhs_ge": "normal", "rhs_le": "uniform"}' + --normal '{"rhs_ge": {"mean": 20, "std": 5}}' + """ + p = argparse.ArgumentParser( + description="Generate uncertainty scenarios from your Pyomo model function." + ) + p.add_argument("--n", type=int, required=True, help="Number of scenarios.") + p.add_argument("--seed", type=int, default=None, help="RNG seed.") + p.add_argument("--out", type=str, required=True, help="Output file (.csv|.json|.parquet).") + p.add_argument( + "--model", + type=str, + default="your_module:simple_lp_model", + help="Model factory spec as 'package.module:function'. Default: your_module:simple_lp_model", + ) + p.add_argument( + "--dist", + type=str, + default=None, + help='JSON mapping param->"uniform"|"normal" (e.g., \'{"rhs_ge":"normal"}\')', + ) + p.add_argument( + "--normal", + type=str, + default=None, + help='JSON mapping param->{"mean":m,"std":s} (e.g., \'{"rhs_ge":{"mean":20,"std":5}}\')', + ) + return p.parse_args(list(argv) if argv is not None else None) + + +# ----------------------------- CLI Entry ----------------------------- # + +def main(argv: Optional[Iterable[str]] = None) -> int: + """ + CLI entry-point. Loads the model factory, generates scenarios, and saves them. + + Returns + ------- + int + Exit code (0 on success). + """ + args = _parse_args(argv) + + # Resolve the model factory (module:function) + model_factory = _load_model_factory(args.model) + + # Build the model and pick up uncertain params and bounds + # Expected: (model, uncertain_params, bounds, xi_set) + model, uncertain_params, bounds, xi_set = model_factory() + + # Parse JSON inputs if provided + per_param_dist = json.loads(args.dist) if args.dist else None + per_param_normal = json.loads(args.normal) if args.normal else None + + df = generate_scenarios_from_model( + uncertain_params=uncertain_params, + bounds=bounds, + n=args.n, + seed=args.seed, + per_param_dist=per_param_dist, + per_param_normal=per_param_normal, + ) + + save_scenarios( + df, + args.out, + meta={ + "model": args.model, + "seed": args.seed, + "dist": per_param_dist, + "normal": per_param_normal, + }, + ) + print(f"[OK] Generated {df.shape[0]} scenarios for {df.shape[1]-1} parameters -> {args.out}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/optichat/tools/search_tool.py b/optichat/tools/search_tool.py new file mode 100644 index 0000000..701e092 --- /dev/null +++ b/optichat/tools/search_tool.py @@ -0,0 +1,118 @@ +from typing import List, Dict, Any +from typing import Optional +import re +import json +from loguru import logger +from google.adk.tools.tool_context import ToolContext +from optichat.config.constants import MODELS_DICTIONARY, MODEL_VERSIONS +from optichat.tools.shortcut_functions import load_model, solve_model + + +def wildcard_to_regex(pattern: str) -> str: + """ + Convert a wildcard pattern to a regex pattern. + The intuition of this function is to escape all regex special characters, as pyomo's component names usually contain "[", "]", "(", ")", etc. + Then only rely on '*' and '?' for wildcard matching. + + Args: + pattern (str): The wildcard pattern to convert. + + Returns: + str: The corresponding regex pattern. + """ + # Escape all regex special characters + regex_pattern = re.escape(pattern) + # Replace escaped wildcards with regex equivalents + regex_pattern = regex_pattern.replace(r'\*', '.*').replace(r'\?', '.') + # Anchor the pattern to match the whole string + return f'^{regex_pattern}$' + + +def get_model_components(version: List[str], component_type: str, pattern: str, + tool_context: ToolContext) -> Dict[str, str]: + """ + get_model_components is a robust and efficient searching method for model components. + This function retrieves information about specified model components that are stored in the session. + + Args: + version (List[str]): Model version(s) to search. Must be provided. Maximum 2 versions allowed. + component_type (str): **PRIMARY METHOD** Type of components to match against component. + Must be one of: ['objective', 'variable', 'constraint', or '' (empty string for all types)] + pattern (str): **BACKUP METHOD** Naming pattern to match against component. + Use this for additional filtering when the output by component_type alone is truncated. + Supports: + - Wildcard patterns: '*' (matches any characters), '?' (matches a single character). + Example: "x_*" matches "x_1", "x_transport", etc. + - Substring matching: Simple text matching (case-insensitive). + Example: "transport" matches "transport", "x_transport_A", "transport_cost", etc. + - Empty string: "" matches all component names. + + Returns: + Dict[str, str]: a dictionary with two keys: "status" and "result" + "status": "success" or "error" + "result": the information about the model components that match the specified version, component_type and pattern + """ + models_dictionary = tool_context.state[MODELS_DICTIONARY].copy() + versions = version + is_valid = True + result = "" + if len(versions) > 2: + is_valid = False + result += "**ERROR** Maximum 2 versions allowed at a time" + if pattern == "" and component_type == "": + is_valid = False + result += "**ERROR** At least one of [pattern, component_type] must be non-empty" + valid_component_types = ['objective', 'variable', 'constraint', ''] + if component_type not in valid_component_types: + is_valid = False + result += (f"**ERROR** component_type must be one of {valid_component_types}, " + f"but got '{component_type}'") + available_versions = list(models_dictionary.keys()) + missing_versions = [v for v in versions if v not in available_versions] + if missing_versions: + is_valid = False + result += (f"**ERROR** available versions are {available_versions}, " + f"but got '{missing_versions}'") + + result_dictionary = {} + # Process each version + for ver in versions: + # Pre-processing: solve if not solved yet + if models_dictionary[ver]["obj"].get("sol_status", "unknown") != "optimal": + model = load_model(ver, models_dictionary) + models_dictionary = solve_model(model, ver, models_dictionary) + tool_context.state[MODELS_DICTIONARY] = models_dictionary + tool_context.state[MODEL_VERSIONS] = list(models_dictionary.keys()) + + # Pattern searching logic + if pattern != "": + info = models_dictionary[ver] + pattern_matches = {} + if '*' in pattern or '?' in pattern: + regex_pattern = wildcard_to_regex(pattern) + compiled_pattern = re.compile(regex_pattern, re.IGNORECASE) + for k in info.keys(): + if compiled_pattern.match(k): + pattern_matches[k] = models_dictionary[ver][k] + else: + for k in info.keys(): + if pattern.lower() in k.lower(): + pattern_matches[k] = models_dictionary[ver][k] + else: + pattern_matches = models_dictionary[ver] + + # Type searching logic + if component_type != "": + type_matches = {} + for comp_name, comp_data in pattern_matches.items(): + if isinstance(comp_data, dict) and comp_data.get("component_type") == component_type: + # Exclude component_type field to save tokens since it's already filtered + comp_data_filtered = {k: v for k, v in comp_data.items() if k != "component_type"} + type_matches[comp_name] = comp_data_filtered + else: + type_matches = pattern_matches + + result_dictionary[ver] = type_matches # type_matches has already applied both pattern and type filtering + + return {"status": "success" if is_valid else "error", + "result": json.dumps(result_dictionary, indent=4) if is_valid else result} \ No newline at end of file diff --git a/optichat/tools/shortcut_functions.py b/optichat/tools/shortcut_functions.py new file mode 100644 index 0000000..20168cf --- /dev/null +++ b/optichat/tools/shortcut_functions.py @@ -0,0 +1,212 @@ +from loguru import logger +import pyomo.environ as pe +from pyomo.opt import SolverFactory, SolverStatus, TerminationCondition +from optichat.tools.extract_tool import extract_model_info, restore_model_object, save_model_object, unique_component_name +from typing import Any, Dict, List, Tuple +import re, json +from optichat.config.constants import USER_QUERY + +def load_model(version: str, models_dictionary: dict): + """ + ```model = load_model(version: str, models_dictionary: dict)``` + loads a model associated with a given version + """ + info = models_dictionary[version] + local_path_to_object = info["local_path_to_object"] + sol_status = info["obj"].get('sol_status', 'unknown') + objval = info["obj"].get('value', 'unknown') + + model, file_name = restore_model_object(local_path_to_object) + + # remove dual suffix if exists so that it won't interfere with newly added constraints + if hasattr(model, 'dual'): + model.del_component(model.dual) + + print(f"Model, in version of {version}, is loaded.") + print(f"{version} Model status: {sol_status}") + print(f"{version} Model optimal objective value: {objval}") + return model + + +def add_dual_suffix(model: pe.ConcreteModel): + """ + ```model_with_dual_suffix = add_dual_suffix(model: pe.ConcreteModel)``` + adds dual suffix to the model so that the resulting model will include dual solution after being solved + """ + if hasattr(model, "dual"): + print("Model already has dual suffix. Original model is returned.") + else: + # simple safeguard to ensure model is LP + for var in model.component_objects(pe.Var, active=True): + for idx in var: + if var[idx].is_binary(): + print(("Model has binary variables. " + "Dual suffix can only be added to LP models. " + "Original model is returned.")) + return model + model.dual = pe.Suffix(direction=pe.Suffix.IMPORT_EXPORT) + return model + + +def solve_model(model: pe.ConcreteModel, version: str, models_dictionary: dict): + """ + ```new_models_dictionary = solve_model(model, version: str, models_dictionary: dict)``` + solves the model and update it in the models_dictionary by labelling it with the given version. + """ + time_limit_seconds = 180 + print(f"Solving model with time limit of {time_limit_seconds} seconds...") + solver = SolverFactory('gurobi') + solver.options['TimeLimit'] = time_limit_seconds + results = solver.solve(model, tee=False) + info = extract_model_info(model, results.solver.termination_condition) + + sol_status = info["obj"].get('sol_status', 'unknown') + objval = info["obj"].get('value', 'unknown') + + if sol_status != "optimal": + print(f"Model, in version of {version}, is NOT solved.") + else: + print(f"Model, in version of {version}, is solved.") + print(f"{version} Model status: {sol_status}") + print(f"{version} Model optimal objective value: {objval}") + + local_path_to_object = save_model_object(model, version) + info.update({ + "local_path_to_object": local_path_to_object, + }) + print(f"Model, in version of {version}, is updated in the models_dictionary.") + + models_dictionary.update({version: info}) + return models_dictionary + + +def relax_constraint_and_penalize_violation(constraint_name: str, + penalty_coef: float | int, + model: pe.ConcreteModel): + """ + ```relaxed_model = relax_constraint_and_penalize_violation(constraint_name: str, penalty_coef: float | int, model)``` + relaxes a constraint in the model by adding slacks and penalizes the violation in the objective in place, returns the relaxed model. + """ + obj = next(model.component_data_objects(pe.Objective, active=True)) + is_min = (obj.sense == pe.minimize) + penalty_sign = 1.0 if is_min else -1.0 + constraint = model.find_component(constraint_name) + if constraint: + if constraint.equality: + us = pe.Var(domain=pe.NonNegativeReals) + model.add_component(unique_component_name(model, f"uslack_{constraint_name}"), us) + ls = pe.Var(domain=pe.NonNegativeReals) + model.add_component(unique_component_name(model, f"lslack_{constraint_name}"), ls) + eqcon = pe.Constraint(expr=(constraint.body == pe.value(constraint.lower) + us - ls)) + model.add_component(unique_component_name(model, f"relaxed_{constraint_name}"), eqcon) + obj.set_value(expr=obj.expr + penalty_sign * penalty_coef * (us + ls)) + elif constraint.has_ub(): + us = pe.Var(domain=pe.NonNegativeReals) + model.add_component(unique_component_name(model, f"uslack_{constraint_name}"), us) + ucon = pe.Constraint(expr=(constraint.body <= pe.value(constraint.upper) + us)) + model.add_component(unique_component_name(model, f"relaxed_{constraint_name}"), ucon) + obj.set_value(expr=obj.expr + penalty_sign * penalty_coef * us) + elif constraint.has_lb(): + ls = pe.Var(domain=pe.NonNegativeReals) + model.add_component(unique_component_name(model, f"lslack_{constraint_name}"), ls) + lcon = pe.Constraint(expr=(constraint.body >= pe.value(constraint.lower) - ls)) + model.add_component(unique_component_name(model, f"relaxed_{constraint_name}"), lcon) + obj.set_value(expr=obj.expr + penalty_sign * penalty_coef * ls) + else: + raise Exception("Constraint has no bounds. No changes made.") + # deactivated constraint can still be found by model.find_component, delete it to avoid confusion + model.del_component(constraint) + print(f"Constraint {constraint_name} is relaxed with slacks.") + print(f"Constraint {constraint_name} violation is penalized in the objective with coefficient {penalty_coef}.") + else: + print(f"Constraint {constraint_name} not found in the model. No changes made.") + return model + + +# Parse user query and return the uncertain parameters and it's bounds +def parse_uncertainty_from_state(state: Dict[str, Any]) -> Tuple[List[str], Dict[str, Tuple[float, float]]]: + """ + Parse uncertainty specification from the most recent user message in `state`. + Priority: + 1) A fenced JSON code block with keys: + {"uncertain_params":[...], "bounds":{"p":[lo,hi], ...}} + 2) Inline fallback, e.g.: + "uncertain: p,q bounds: p[0,10]; q[-5,5]" or "p in [0,10]" + Returns (uncertain_params, bounds) where bounds[k] = (lo, hi) as floats. + If nothing is found, returns ([], {}). + """ + text = (state.get(USER_QUERY) or "").strip() + if not text: + return [], {} + + # 1) JSON block (preferred) + m = re.search(r"```(?:json)?\s*({.*?})\s*```", text, re.DOTALL) + if m: + try: + blob = json.loads(m.group(1)) + up = blob.get("uncertain_params") or blob.get("uncertain") or [] + bd = blob.get("bounds") or {} + up = [str(u) for u in up] + bounds = {k: (float(v[0]), float(v[1])) for k, v in bd.items()} + if up or bounds: + return up, bounds + except Exception: + pass + + # 2) Inline fallback + up: List[str] = [] + bdict: Dict[str, Tuple[float, float]] = {} + + mup = re.search(r"uncertain(?:\s*params)?\s*:\s*([A-Za-z0-9_,\s]+)", text, re.IGNORECASE) + if mup: + up = [u.strip() for u in mup.group(1).split(",") if u.strip()] + + # patterns like p[0,10] | p in [0,10] | demand[0,1e3] + for name, lo, hi in re.findall( + r"([A-Za-z_]\w*)\s*(?:in)?\s*\[\s*([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)\s*,\s*([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)\s*\]", + text, + ): + bdict[name] = (float(lo), float(hi)) + + return up, bdict + + + +# def fix_variable(variable_name: str, value_to_fix: float | int, model: pe.ConcreteModel): +# """ +# ```fix_variable(variable_name: str, value_to_fix: float | int, model)``` +# fixes a variable in the model to a given value in place, returns nothing. +# """ +# var = model.find_component(variable_name) +# var.fix(value_to_fix) + + +# def unfix_variable(variable_name: str, model: pe.ConcreteModel): +# """ +# ```unfix_variable(variable_name: str, model)``` +# unfixes a variable in the model in place, returns nothing. +# """ +# var = model.find_component(variable_name) +# var.unfix() + + +# def add_constraint(constraint_name: str, expression: str, model: pe.ConcreteModel): +# """ +# ```add_constraint(constraint_name: str, expression: str, model)``` +# adds a constraint to the model in place, returns nothing. +# TODO: the most difficult part +# reconstruct pyomo expression from string +# - model.find_component(component_name) can get the actual pyomo component +# - need a way to rearrange the components into a valid pyomo expression from expression string +# need a way to parse indexed expression into pyomo rule function +# """ +# pass + + +# def deactivate_constraint(constraint_name: str, model: pe.ConcreteModel): +# """ +# ```deactivate_constraint(constraint_name: str, model)``` +# deactivates a constraint in the model in place, returns nothing. +# """ +# constraint = model.find_component(constraint_name) +# constraint.deactivate() \ No newline at end of file diff --git a/robust_results.csv b/robust_results.csv new file mode 100644 index 0000000..ce67392 --- /dev/null +++ b/robust_results.csv @@ -0,0 +1,11 @@ +scenario_id,"demand[1,1]","demand[2,1]",objective,"inv_bal[0,0]","inv_bal[0,1]","inv_bal[0,2]","inv_bal[0,3]","inv_bal[0,4]","inv_bal[0,5]","inv_bal[0,6]","inv_bal[1,0]","inv_bal[1,1]","inv_bal[1,2]","inv_bal[1,3]","inv_bal[1,4]","inv_bal[1,5]","inv_bal[1,6]","inv_bal[2,0]","inv_bal[2,1]","inv_bal[2,2]","inv_bal[2,3]","inv_bal[2,4]","inv_bal[2,5]","inv_bal[2,6]","inv_bal[3,0]","inv_bal[3,1]","inv_bal[3,2]","inv_bal[3,3]","inv_bal[3,4]","inv_bal[3,5]","inv_bal[3,6]","inv_bal[4,0]","inv_bal[4,1]","inv_bal[4,2]","inv_bal[4,3]","inv_bal[4,4]","inv_bal[4,5]","inv_bal[4,6]","inv_bal[5,0]","inv_bal[5,1]","inv_bal[5,2]","inv_bal[5,3]","inv_bal[5,4]","inv_bal[5,5]","inv_bal[5,6]","pipe_bal[0,2,0]","pipe_bal[0,2,1]","pipe_bal[0,3,0]","pipe_bal[0,3,1]","pipe_bal[0,4,1]","pipe_bal[0,5,2]","pipe_bal[0,5,3]","pipe_bal[0,5,4]","pipe_bal[0,6,2]","pipe_bal[0,6,4]","pipe_bal[0,7,5]","pipe_bal[0,7,6]","pipe_bal[0,8,5]","pipe_bal[0,8,6]","pipe_bal[1,2,0]","pipe_bal[1,2,1]","pipe_bal[1,3,0]","pipe_bal[1,3,1]","pipe_bal[1,4,1]","pipe_bal[1,5,2]","pipe_bal[1,5,3]","pipe_bal[1,5,4]","pipe_bal[1,6,2]","pipe_bal[1,6,4]","pipe_bal[1,7,5]","pipe_bal[1,7,6]","pipe_bal[1,8,5]","pipe_bal[1,8,6]","pipe_bal[2,2,0]","pipe_bal[2,2,1]","pipe_bal[2,3,0]","pipe_bal[2,3,1]","pipe_bal[2,4,1]","pipe_bal[2,5,2]","pipe_bal[2,5,3]","pipe_bal[2,5,4]","pipe_bal[2,6,2]","pipe_bal[2,6,4]","pipe_bal[2,7,5]","pipe_bal[2,7,6]","pipe_bal[2,8,5]","pipe_bal[2,8,6]","pipe_bal[3,2,0]","pipe_bal[3,2,1]","pipe_bal[3,3,0]","pipe_bal[3,3,1]","pipe_bal[3,4,1]","pipe_bal[3,5,2]","pipe_bal[3,5,3]","pipe_bal[3,5,4]","pipe_bal[3,6,2]","pipe_bal[3,6,4]","pipe_bal[3,7,5]","pipe_bal[3,7,6]","pipe_bal[3,8,5]","pipe_bal[3,8,6]","pipe_bal[4,2,0]","pipe_bal[4,2,1]","pipe_bal[4,3,0]","pipe_bal[4,3,1]","pipe_bal[4,4,1]","pipe_bal[4,5,2]","pipe_bal[4,5,3]","pipe_bal[4,5,4]","pipe_bal[4,6,2]","pipe_bal[4,6,4]","pipe_bal[4,7,5]","pipe_bal[4,7,6]","pipe_bal[4,8,5]","pipe_bal[4,8,6]","pipe_bal[5,2,0]","pipe_bal[5,2,1]","pipe_bal[5,3,0]","pipe_bal[5,3,1]","pipe_bal[5,4,1]","pipe_bal[5,5,2]","pipe_bal[5,5,3]","pipe_bal[5,5,4]","pipe_bal[5,6,2]","pipe_bal[5,6,4]","pipe_bal[5,7,5]","pipe_bal[5,7,6]","pipe_bal[5,8,5]","pipe_bal[5,8,6]","re_cap[1,5]","re_cap[1,6]","re_cap[1,7]","re_cap[1,8]","re_cap[2,5]","re_cap[2,6]","re_cap[2,7]","re_cap[2,8]","re_cap[3,5]","re_cap[3,6]","re_cap[3,7]","re_cap[3,8]","re_cap[4,5]","re_cap[4,6]","re_cap[4,7]","re_cap[4,8]","re_cap[5,5]","re_cap[5,6]","re_cap[5,7]","re_cap[5,8]","re_inv[1,2]","re_inv[1,3]","re_inv[1,4]","re_inv[1,5]","re_inv[1,6]","re_inv[2,2]","re_inv[2,3]","re_inv[2,4]","re_inv[2,5]","re_inv[2,6]","re_inv[3,2]","re_inv[3,3]","re_inv[3,4]","re_inv[3,5]","re_inv[3,6]","re_inv[4,2]","re_inv[4,3]","re_inv[4,4]","re_inv[4,5]","re_inv[4,6]","re_inv[5,2]","re_inv[5,3]","re_inv[5,4]","re_inv[5,5]","re_inv[5,6]","sales1[1,0]","sales1[1,1]","sales1[2,0]","sales1[2,1]","sales1[3,0]","sales1[3,1]","sales1[4,0]","sales1[4,1]","sales1[5,0]","sales1[5,1]","sales2[1,0]","sales2[1,1]","sales2[2,0]","sales2[2,1]","sales2[3,0]","sales2[3,1]","sales2[4,0]","sales2[4,1]","sales2[5,0]","sales2[5,1]","backlog[1,0]","backlog[1,1]","backlog[2,0]","backlog[2,1]","backlog[3,0]","backlog[3,1]","backlog[4,0]","backlog[4,1]","backlog[5,0]","backlog[5,1]",final_inv[0],final_inv[1],final_inv[2],final_inv[3],final_inv[4],final_inv[5],final_inv[6] +1,14.189367762356397,15.129593039686874,-6.199999999999989,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0 +2,12.431264363787175,19.847097040330787,-6.199999999999989,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0 +3,16.84109275977749,10.483798660914147,-6.199999999999989,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0 +4,16.82807319809983,12.61671288092458,-6.199999999999989,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0 +5,15.783827841498205,11.24189945576181,-6.199999999999989,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0 +6,17.862431361180523,10.126758453679015,-6.199999999999989,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0 +7,12.972592353897713,12.404714006100827,-6.199999999999989,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0 +8,15.177920166682386,18.75807923235374,-6.199999999999989,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0 +9,13.126934705888154,10.760506399244615,-6.199999999999989,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0 +10,13.605377425754025,19.37873131112965,-6.199999999999989,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0 diff --git a/robust_results.csv.scenarios.csv b/robust_results.csv.scenarios.csv new file mode 100644 index 0000000..8f2fa3b --- /dev/null +++ b/robust_results.csv.scenarios.csv @@ -0,0 +1,11 @@ +scenario_id,"demand[1,1]","demand[2,1]" +1,14.189367762356397,15.129593039686874 +2,12.431264363787175,19.847097040330787 +3,16.84109275977749,10.483798660914147 +4,16.82807319809983,12.61671288092458 +5,15.783827841498205,11.24189945576181 +6,17.862431361180523,10.126758453679015 +7,12.972592353897713,12.404714006100827 +8,15.177920166682386,18.75807923235374 +9,13.126934705888154,10.760506399244615 +10,13.605377425754025,19.37873131112965 diff --git a/utils.py b/utils.py deleted file mode 100644 index a341b19..0000000 --- a/utils.py +++ /dev/null @@ -1,100 +0,0 @@ -import json -import time -import copy -import typing -import os -import sys -import re -import importlib -# Streamlit -import streamlit as st -# Gurobi -import pyomo.environ as pe -from pyomo.opt import SolverFactory -from pyomo.contrib.iis import * -import re -from pyomo.core.expr.visitor import identify_mutable_parameters, replace_expressions, clone_expression -# GPT -from openai import OpenAI -from dotenv import load_dotenv, find_dotenv -_ = load_dotenv(find_dotenv()) # read local .env file -import tiktoken - - -from prompts import get_prompts, get_tools, get_syntax_guidance_tool -from agents import Interpreter, Coordinator, Explainer, Engineer - - -def get_agents(fn_names, client, llm='gpt-4-turbo-preview'): - interpreter = Interpreter(client=client, llm=llm) - explainer = Explainer(client=client, llm=llm) - - multiple_tools, single_tools, none_tools, all_tools, tool_choice = get_tools(fn_names) - syntax_guidance_tool = get_syntax_guidance_tool() - engineer = Engineer(client=client, llm=llm, - multiple_tools=multiple_tools, single_tools=single_tools, - none_tools=none_tools, all_tools=all_tools, - tool_choice=tool_choice, - syntax_guidance_tool=syntax_guidance_tool, - function_names=str(fn_names)) - coordinator = Coordinator(client=client, agents=[explainer, engineer], llm=llm) - return interpreter, explainer, engineer, coordinator - - -def save_team_conversation(team_conversation, filename): - with open(filename, 'w') as f: - for message in team_conversation: - f.write(json.dumps(message) + '\n') - - -def OptiChat_workflow_exp(args, coordinator, engineer, explainer, messages, models_dict): - team_conversation = [] - rounds = 0 - - # set the time in agents to 0 - coordinator.coordination_time = 0 - engineer.syntax_time = 0 - engineer.programing_time = 0 - engineer.evaluation_time = 0 - explainer.explanation_time = 0 - - # in current design, if coordinator has assigned the task once, - # actually there will be no need to call llm to generate the decision again - while rounds <= coordinator.max_rounds: - coordinator_start = time.time() - decision = coordinator.generate_decision_exp(args, messages, team_conversation) - coordinator_end = time.time() - coordinator.coordination_time += (coordinator_end - coordinator_start) - - if not decision: - print(f'coordinator failed to generate decision') - messages.append({"role": "assistant", "content": "LLM failed"}) - return messages, team_conversation - - else: - if decision["agent_name"] == 'Engineer': - # unlike explainer, engineer team has already updated the team_conversation and messages in fn below - # syntax time, programming time, evaluation time are also updated in the fn below - messages, team_conversation = engineer.generate_report_exp(args, - messages, team_conversation, models_dict) - - elif decision["agent_name"] == "Explainer": - explainer_start = time.time() - explanation = explainer.generate_explanation_exp(args, messages, team_conversation) - if args.explanation_stream: - with st.chat_message("assistant"): - explanation_response = st.write_stream(explanation) - else: - explanation_response = explanation - explainer_end = time.time() - explainer.explanation_time += (explainer_end - explainer_start) - - team_conversation.append({"agent_name": "Explainer", "agent_response": explanation_response}) - messages.append({"role": "assistant", "content": explanation_response}) - return messages, team_conversation - - else: - raise ValueError( - f"Decision {decision} has an invalid agent name. Please choose from Engineer or Explainer.") - - rounds += 1