Skip to content

4. Developing the API (v3)

Bill Becker edited this page Feb 14, 2024 · 5 revisions

A guide for adding models from REopt.jl into the new REopt API v3. Please first review 1. Structure of the API.

Contribution guidelines

1. Set up a minimum working example

Create a minimal testcase which utilizes the model to be added into API v3. Observe the possible inputs, default values when providing inputs, as well as the output dictionary from REopt.jl. Also pay attention to the REopt.jl version or Github branch being used in this test.

  • The input dictionary from this step will be used to validate proper functioning of API v3 post modifications.
  • The Julia version/branch used in this step will also be used to test the model during and after development (never push REopt_API\julia_src\ TOML files if they point to your locally dev'ed REopt.jl). Additional considerations:
  1. What model are you trying to bring into JOB?
  • Open REO -> nested_inputs and REO -> nested_outputs files along with REO -> Validators.py and collect information on this functionality as ewll as validations against the new model.
  • Find a test case: check the REopt.jl branch which you will be developing / testing against for a test case json file.

2. Docker launch and development

Launch docker containers in nojul configuration, launch julia http server, and open JOBS-> Models.py file. Point the TOML files inside REopt_API\julia_src\ folder to the correct REopt.jl version or Github branch (see Julia package manager for more details).

  1. Create an outline for the class that will be added based on existing input and output classes in Models.py
  2. Make sure to include the clean function with pass for the time being.
  3. Update the following files with your new classes. ExistingBoilerInputs and ExistingBoilerOutputs are being used as example here:

Jobs -> Models.py

  • update get_input_dict_from_run_uuid

Jobs -> Validators.py

  • Add input class to imports
  • Update self.objects in InputValidator class

Jobs -> Views.py

  • Add input and output classes to import statement
  • Add input class to help() function
  • Add output class to outputs function
  • Go through results function and add both input and output classes as needed

Jobs -> process_results.py

  • Add output class to imports and update process_results function
  1. Add the fields that need to be transferred to new classes in models.py. If a large number of fields are being added, it is ideal to do this step in batches in conjunction with steps 3 and 4.

Please periodically check docker outputs. If there are any errors in datatypes (such as missing default values for a field which can't be blank or null) they show up in this prompt. One can also run a migration and restart docker to ensure all their new fields have taken affect and there are no bugs.

Next, we need to align the files created under JOBS -> Migrations with whats available here: https://github.com/NREL/REopt_API/tree/master/job/migrations. The idea is that all your changes will be included in one migration file so as to cleanly build on top of migrations already in the Master branch.

  • To do so, in your branch delete the migration files which have been created locally so as to match your migrations folder with whats in Master.
  • Run and apply a migration. This will make sure all your changes are neatly captured in a single migration file without conflicting with whats in production.
  • No need to restart docker after a migration.

3. Add field validations

Validations allow the API to accept/reject requests based on other input fields.

For example, fuel_cost_per_mmbtu can be provided as a scalar, or a vector in REopt.jl. However, in the API it is saved as a vector. Therefore, if the input contains fuel_cost_per_mmbtu, the API updates this field to become 8760 in length if its initial length is 1 (i.e. scalar value). Similarly, if a vector of 12 numbers is provided, the API correctly scales this vector up to 8760 in length.

Validations can also be done between models via cross-clean functionality. For example, if the input has Settings.time_steps_per_hour = 4, then fuel_cost_per_mmbtu can be scaled to length 35040.

  • Refer to REO->Validators.py for any existing validations that need to be transferred over. You can also check the comments in Julia code on required input fields or possible set of input fields.
  • Add any intra-model validations to clean method within the class
  • Add any inter-model validations to cross-clean method in validators.py file
  • Be sure to test the API frequently so as to not break anything!

4. Test the changes

Once fields and models have been added, ensure that results are same via API and directly with Julia. Use the JSON request from step 1 in Postman to ensure the API is giving the same response as REopt.jl. The responses can differ if there are known differences between the API and REopt.jl.

Issues?

  • Existing documentation on troubleshooting Docker development are very helpful if the JSON should work but isnt working.
  • Sometimes if many changes have been made, it might be a good idea to restart docker and compose the containers again.
  • When docker-compose command runs, pay attention to the output. It can contain ERROR statements from postgres db where you might need to delete the db and recreate it.
  • If you have an instance where a POST works directly with Julia but returns infeasible model with the API, do the following: -- Send a GET with the failed run_uuid to see how the API populated/processed default fields -- Paste the techs of concern from GET response into your baseline test case and run directly with Julia package. You should get the same error. Now you can troubleshoot the error outside the API and make adjustments to Model.py accordingly.

v3 (REopt.jl integration) troubleshooting

  1. When adding new outputs to API V3 (job app), these fields need to align perfectly with the outputs from REopt.jl; if not, the API response will just omit the output model/table because it silently errors when trying to create that
  • This makes it a little tricky to debug – I’ve been writing the http.jl /reopt output to JSON and compare response names with [Tech]Outputs Django model fields
    • Maybe we should add some error message when it tries and fails to create a [Tech]Outputs model.
  1. The API POST to REopt.jl includes all of the default values assigned in models.py, so this is not one-to-one with the equivalent inputs that you’d sent to REopt.jl directly.
  • When I ran into some unexpected errors, I found it useful to try to run REopt.jl directly with the API POST as inputs. This is somewhat referred to in the “Issues” section in the link above.
  1. When using docker-compose.nojulia.yaml and spinning up local machines Julia environment, running http.jl with local dev’d REopt.jl, note:
  • This is not necessarily the same exact environment as the Docker’s Julia src because it could be using a different version of Julia (specified in the julia_src/Dockerfile.yml)
    • So if you update your environment from your local Julia vs inside the Docker container Julia service, you might get different dependencies which may be incompatible
    • Relatedly, I’m proposing to update to Julia v1.8 which should be consistent across REopt.jl, the julia_src/Dockerfile.yml, julia_src Manifest.yml, and the GitHub workflows CI.yaml file for running tests on GitHub
  • The Julia logging and error message passing seems to be lacking a little in this mode, compared to seeing the logs in the Julia Docker container
    • I could be wrong on this because I was mainly stuck on the Outputs model erroring silently, so want to check that the error passing from Julia should still work with this setup.
  1. Julia optimization model debugging: if there are unhandled errors in the JuMP model, REopt errors “ungracefully” here, so it does not log JuMP errors in the response.
  • I found it useful to write the dictionary response right before the combine_results function call to view the JuMP errors. We should fix this to avoid calling the function that would error and instead return the JuMP error in the errors key.

Validations

  1. clean_fields - django's inbuilt capability which validates input fields
  2. clean - custom validation within an input / output class
  3. cross_clean - validation across input classes (see cross_clean() under validators.py. Example: make sure all values are upsampled or downsampled per Settings.time_steps_per_hour.
  4. Refer to REO->Validators.py for any existing validations that need to be transferred over.

PostgreSQL Tips

Creating new inputs to the API and working with Django migrations can be confusing at first. If you totally bugger up your local PostgreSQL database during development, log into psql and:

(WARNING the following steps will wipe your reopt database!)

drop database reopt;
create database reopt;

Then in your development environment:

python manage.py migrate

If you are using Docker containers, the easiest way to wipe the database is:

For HiGHS, Cbc, and SCIP solvers:

docker compose down -v

For Xpress solver:

docker compose -f docker-compose.xpress.yml down -v

This removes all API containers and deletes the volume (-v) that stores the PostgreSQL database. The database will be recreated when you:

For HiGHS, Cbc, and SCIP solvers:

docker compose up

For Xpress solver:

docker compose -f docker-compose.xpress.yml up

Working with PostgreSQL can be tricky. Here are a few good resources: link1 link2