From 07bb0316ba9f26a987cdb3760441f2ed94d9a53e Mon Sep 17 00:00:00 2001 From: Paolo Tormene Date: Fri, 21 Feb 2025 16:44:15 +0100 Subject: [PATCH 1/6] Fix management of dip, rake and strike and pre-build a rupture using the first available nodal plane --- openquake/hazardlib/shakemap/parsers.py | 23 ++++++++------- .../hazardlib/tests/shakemap/parsers_test.py | 29 +++++++++++++++++-- .../hazardlib/tests/shakemap/validate_test.py | 7 ----- openquake/server/static/js/engine.js | 2 -- 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/openquake/hazardlib/shakemap/parsers.py b/openquake/hazardlib/shakemap/parsers.py index ab45c916ea2..901b6a974b3 100644 --- a/openquake/hazardlib/shakemap/parsers.py +++ b/openquake/hazardlib/shakemap/parsers.py @@ -828,9 +828,19 @@ def get_rup_dic(dic, user=User(), approach='use_shakemap_from_usgs', if err: return None, None, err if approach in ['use_pnt_rup_from_usgs', 'build_rup_from_usgs']: - rupdic, err = load_rupdic_from_origin(usgs_id, properties['products']) - if err: - return None, None, err + if dic.get('lon', None) is None: # do not override user-inserted values if present + rupdic, err = load_rupdic_from_origin(usgs_id, properties['products']) + if err: + return None, None, err + if approach == 'build_rup_from_usgs': + rupdic['nodal_planes'], err = _get_nodal_planes(properties) + if err: + return None, None, err + else: + rupdic.update(rupdic['nodal_planes']['NP1']) + else: + rupdic = dic.copy() + rupdic['require_dip_strike'] = True elif ('download/rupture.json' not in contents or approach == 'use_finite_rup_from_usgs'): # happens for us6000f65h in parsers_test @@ -839,13 +849,6 @@ def get_rup_dic(dic, user=User(), approach='use_shakemap_from_usgs', if err: return None, None, err - if approach == 'build_rup_from_usgs': - rupdic['nodal_planes'], err = _get_nodal_planes(properties) - rupdic['aspect_ratio'] = dic['aspect_ratio'] - rupdic['msr'] = dic['msr'] - if err: - return None, rupdic, err - if not rup_data and approach not in ['use_pnt_rup_from_usgs', 'build_rup_from_usgs']: with monitor('Downloading rupture json'): diff --git a/openquake/hazardlib/tests/shakemap/parsers_test.py b/openquake/hazardlib/tests/shakemap/parsers_test.py index a394f33e9fb..eca361d05a6 100644 --- a/openquake/hazardlib/tests/shakemap/parsers_test.py +++ b/openquake/hazardlib/tests/shakemap/parsers_test.py @@ -106,7 +106,7 @@ def test_6(self): def test_7(self): dic_in = {'usgs_id': 'us6000jllz', 'lon': None, 'lat': None, 'dep': None, - 'mag': None, 'msr': '', 'aspect_ratio': 2.0, 'rake': None, + 'mag': None, 'msr': '', 'aspect_ratio': 2, 'rake': None, 'dip': None, 'strike': None} _rup, dic, _err = get_rup_dic( dic_in, user=user, approach='build_rup_from_usgs', use_shakemap=True) @@ -114,8 +114,6 @@ def test_7(self): dic['nodal_planes'], {'NP1': {'dip': 88.71, 'rake': -179.18, 'strike': 317.63}, 'NP2': {'dip': 89.18, 'rake': -1.29, 'strike': 227.61}}) - self.assertEqual(dic['msr'], '') - self.assertEqual(dic['aspect_ratio'], 2.0) def test_8(self): dic_in = {'usgs_id': 'us6000jllz', 'lon': 37.0143, 'lat': 37.2256, @@ -126,6 +124,31 @@ def test_8(self): self.assertAlmostEqual(rup.surface.length, 0.0133224) self.assertAlmostEqual(rup.surface.width, 0.0070800) + def test_9(self): + dic_in = {'usgs_id': 'us6000jllz', 'lon': 37.0143, 'lat': 37.2256, 'dep': 10, + 'mag': 7.8, 'msr': 'WC1994', 'aspect_ratio': 3, + 'rake': -179.18, 'dip': 88.71, 'strike': 317.63} + _rup, dic, _err = get_rup_dic( + dic_in, user=user, approach='build_rup_from_usgs', use_shakemap=True) + self.assertEqual(dic['dep'], 10) + self.assertEqual(dic['dip'], 88.71) + self.assertEqual(dic['lat'], 37.2256) + self.assertEqual(dic['lon'], 37.0143) + self.assertEqual(dic['mag'], 7.8) + self.assertEqual(dic['msr'], 'WC1994') + self.assertEqual(dic['rake'], -179.18) + self.assertEqual(dic['strike'], 317.63) + self.assertEqual(dic['require_dip_strike'], True) + self.assertEqual(dic['aspect_ratio'], 3) + + def test_10(self): + dic_in = {'usgs_id': 'us6000jllz', 'lon': 37.0143, 'lat': 37.2256, 'dep': 10.0, + 'mag': 7.8, 'msr': 'WC1994', 'aspect_ratio': 2.0, + 'rake': -179.18, 'dip': 88.71, 'strike': 317.63} + _rup, _dic, err = get_rup_dic( + dic_in, user=user, approach='build_rup_from_usgs', use_shakemap=True) + self.assertIn('The depth must be greater', err['error_msg']) + """ NB: to profile a test you can use diff --git a/openquake/hazardlib/tests/shakemap/validate_test.py b/openquake/hazardlib/tests/shakemap/validate_test.py index 2f97c2c13b6..a1da1d507a6 100644 --- a/openquake/hazardlib/tests/shakemap/validate_test.py +++ b/openquake/hazardlib/tests/shakemap/validate_test.py @@ -61,13 +61,6 @@ def test_1b(self): self.assertIn('stations', rupdic['station_data_file']) self.assertEqual(err, {}) - def test_1c(self): - # giving a ValueError with aspect_ratio 2 - POST = {'usgs_id': 'us6000jllz', 'approach': 'build_rup_from_usgs', - 'msr': 'WC1994', 'aspect_ratio': '2'} - _rup, _rupdic, _params, err = impact_validate(POST, user) - self.assertIn('The depth must be greater', err['error_msg']) - def test_2(self): POST = {'usgs_id': 'us7000n05d', 'approach': 'build_rup_from_usgs', 'msr': ''} diff --git a/openquake/server/static/js/engine.js b/openquake/server/static/js/engine.js index b51c45ee880..6b11df97ff2 100644 --- a/openquake/server/static/js/engine.js +++ b/openquake/server/static/js/engine.js @@ -751,8 +751,6 @@ function capitalizeFirstLetter(val) { else if (data.require_dip_strike) { $('#dip').prop('disabled', false); $('#strike').prop('disabled', false); - $('#dip').val('90'); - $('#strike').val('0'); } else { $('#dip').prop('disabled', true); $('#strike').prop('disabled', true); From e33597729621ee96d815d3f7228caff300229580 Mon Sep 17 00:00:00 2001 From: Paolo Tormene Date: Fri, 21 Feb 2025 16:50:19 +0100 Subject: [PATCH 2/6] Do not require dip and strike when using a point rupture --- openquake/hazardlib/shakemap/parsers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openquake/hazardlib/shakemap/parsers.py b/openquake/hazardlib/shakemap/parsers.py index 901b6a974b3..dda69633f72 100644 --- a/openquake/hazardlib/shakemap/parsers.py +++ b/openquake/hazardlib/shakemap/parsers.py @@ -811,7 +811,6 @@ def get_rup_dic(dic, user=User(), approach='use_shakemap_from_usgs', except ValueError as exc: err = {"status": "failed", "error_msg": str(exc)} return rup, rupdic, err - if rupture_file: if rupture_file.endswith('.xml'): rup, rupdic, err = _get_rup_dic_from_xml( @@ -833,6 +832,7 @@ def get_rup_dic(dic, user=User(), approach='use_shakemap_from_usgs', if err: return None, None, err if approach == 'build_rup_from_usgs': + rupdic['require_dip_strike'] = True rupdic['nodal_planes'], err = _get_nodal_planes(properties) if err: return None, None, err @@ -840,7 +840,6 @@ def get_rup_dic(dic, user=User(), approach='use_shakemap_from_usgs', rupdic.update(rupdic['nodal_planes']['NP1']) else: rupdic = dic.copy() - rupdic['require_dip_strike'] = True elif ('download/rupture.json' not in contents or approach == 'use_finite_rup_from_usgs'): # happens for us6000f65h in parsers_test From 47c432f13a49e1dd1d278f69c4a08ea41a460fbf Mon Sep 17 00:00:00 2001 From: Paolo Tormene Date: Sat, 22 Feb 2025 18:10:49 +0100 Subject: [PATCH 3/6] Revert "Do not require dip and strike when using a point rupture" This reverts commit e33597729621ee96d815d3f7228caff300229580. --- openquake/hazardlib/shakemap/parsers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openquake/hazardlib/shakemap/parsers.py b/openquake/hazardlib/shakemap/parsers.py index dda69633f72..901b6a974b3 100644 --- a/openquake/hazardlib/shakemap/parsers.py +++ b/openquake/hazardlib/shakemap/parsers.py @@ -811,6 +811,7 @@ def get_rup_dic(dic, user=User(), approach='use_shakemap_from_usgs', except ValueError as exc: err = {"status": "failed", "error_msg": str(exc)} return rup, rupdic, err + if rupture_file: if rupture_file.endswith('.xml'): rup, rupdic, err = _get_rup_dic_from_xml( @@ -832,7 +833,6 @@ def get_rup_dic(dic, user=User(), approach='use_shakemap_from_usgs', if err: return None, None, err if approach == 'build_rup_from_usgs': - rupdic['require_dip_strike'] = True rupdic['nodal_planes'], err = _get_nodal_planes(properties) if err: return None, None, err @@ -840,6 +840,7 @@ def get_rup_dic(dic, user=User(), approach='use_shakemap_from_usgs', rupdic.update(rupdic['nodal_planes']['NP1']) else: rupdic = dic.copy() + rupdic['require_dip_strike'] = True elif ('download/rupture.json' not in contents or approach == 'use_finite_rup_from_usgs'): # happens for us6000f65h in parsers_test From 598d00b18e8dd69865bdc86341991d4622d5dd2a Mon Sep 17 00:00:00 2001 From: Paolo Tormene Date: Mon, 24 Feb 2025 10:01:59 +0100 Subject: [PATCH 4/6] Fix pep8 --- openquake/hazardlib/shakemap/parsers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openquake/hazardlib/shakemap/parsers.py b/openquake/hazardlib/shakemap/parsers.py index 901b6a974b3..8cee539709a 100644 --- a/openquake/hazardlib/shakemap/parsers.py +++ b/openquake/hazardlib/shakemap/parsers.py @@ -828,7 +828,7 @@ def get_rup_dic(dic, user=User(), approach='use_shakemap_from_usgs', if err: return None, None, err if approach in ['use_pnt_rup_from_usgs', 'build_rup_from_usgs']: - if dic.get('lon', None) is None: # do not override user-inserted values if present + if dic.get('lon', None) is None: # don't override user-inserted values rupdic, err = load_rupdic_from_origin(usgs_id, properties['products']) if err: return None, None, err @@ -852,8 +852,7 @@ def get_rup_dic(dic, user=User(), approach='use_shakemap_from_usgs', if not rup_data and approach not in ['use_pnt_rup_from_usgs', 'build_rup_from_usgs']: with monitor('Downloading rupture json'): - rup_data, rupture_file = download_rupture_data( - usgs_id, contents, user) + rup_data, rupture_file = download_rupture_data(usgs_id, contents, user) if not rupdic: rupdic = convert_rup_data(rup_data, usgs_id, rupture_file, shakemap) if (approach != 'use_shakemap_from_usgs' and not station_data_file From bbc5d430b3aa3e1bce16242a0e7b9a1ea5f00e57 Mon Sep 17 00:00:00 2001 From: Paolo Tormene Date: Mon, 24 Feb 2025 11:12:28 +0100 Subject: [PATCH 5/6] Use the validator also for approach and msr; add a test to check and expected error in 'provide_rup_params' --- openquake/hazardlib/shakemap/parsers.py | 6 +- openquake/hazardlib/shakemap/validate.py | 16 +++-- .../hazardlib/tests/shakemap/parsers_test.py | 59 +++++++++++-------- 3 files changed, 46 insertions(+), 35 deletions(-) diff --git a/openquake/hazardlib/shakemap/parsers.py b/openquake/hazardlib/shakemap/parsers.py index 8cee539709a..cdaafbd82ad 100644 --- a/openquake/hazardlib/shakemap/parsers.py +++ b/openquake/hazardlib/shakemap/parsers.py @@ -773,7 +773,7 @@ def _get_rup_from_json(usgs_id, rupture_file, station_data_file): return rup, rupdic, rup_data -def get_rup_dic(dic, user=User(), approach='use_shakemap_from_usgs', +def get_rup_dic(dic, user=User(), use_shakemap=False, rupture_file=None, station_data_file=None, download_usgs_stations=True, monitor=performance.Monitor()): @@ -787,8 +787,6 @@ def get_rup_dic(dic, user=User(), approach='use_shakemap_from_usgs', :param dic: dictionary with ShakeMap ID and other parameters :param user: User instance - :param approach: the workflow selected by the user - (default: 'use_shakemap_from_usgs') :param use_shakemap: download the ShakeMap only if True :param rupture_file: None :param station_data_file: None @@ -800,6 +798,7 @@ def get_rup_dic(dic, user=User(), approach='use_shakemap_from_usgs', rup_data = {} err = {} usgs_id = dic['usgs_id'] + approach = dic['approach'] rup = None if approach == 'provide_rup_params': rupdic = dic.copy() @@ -811,7 +810,6 @@ def get_rup_dic(dic, user=User(), approach='use_shakemap_from_usgs', except ValueError as exc: err = {"status": "failed", "error_msg": str(exc)} return rup, rupdic, err - if rupture_file: if rupture_file.endswith('.xml'): rup, rupdic, err = _get_rup_dic_from_xml( diff --git a/openquake/hazardlib/shakemap/validate.py b/openquake/hazardlib/shakemap/validate.py index 85b14f78947..cf8a39693cd 100644 --- a/openquake/hazardlib/shakemap/validate.py +++ b/openquake/hazardlib/shakemap/validate.py @@ -163,11 +163,18 @@ def get_oqparams(self, usgs_id, mosaic_models, trts, use_shakemap): } validators = { + 'approach': valid.Choice('use_shakemap_from_usgs', + 'use_pnt_rup_from_usgs', + 'build_rup_from_usgs', + 'use_finite_rup_from_usgs', + 'provide_rup', + 'provide_rup_params'), 'usgs_id': valid.simple_id, 'lon': valid.longitude, 'lat': valid.latitude, 'dep': valid.positivefloat, 'mag': valid.positivefloat, + 'msr': valid.utf8, 'aspect_ratio': valid.positivefloat, 'rake': valid.rake_range, 'dip': valid.dip_range, @@ -190,7 +197,7 @@ def _validate(POST): validation_errs = {} invalid_inputs = [] params = {} - dic = dict(usgs_id=None, lon=None, lat=None, dep=None, + dic = dict(approach=None, usgs_id=None, lon=None, lat=None, dep=None, mag=None, msr=None, aspect_ratio=None, rake=None, dip=None, strike=None) for field, validation_func in validators.items(): if field not in POST: @@ -198,7 +205,7 @@ def _validate(POST): try: value = validation_func(POST.get(field)) except Exception as exc: - blankable = ['dip', 'strike', + blankable = ['dip', 'strike', 'msr', 'maximum_distance_stations', 'local_timestamp'] if field in blankable and POST.get(field) == '': if field in dic: @@ -272,12 +279,9 @@ def impact_validate(POST, user, rupture_file=None, station_data_file=None, use_shakemap = user.level == 1 if 'use_shakemap' in POST: use_shakemap = POST['use_shakemap'] == 'true' - approach = POST['approach'] - if approach == 'build_rup_from_usgs': - dic['msr'] = POST['msr'] rup, rupdic, err = get_rup_dic( - dic, user, approach, use_shakemap, rupture_file, station_data_file, + dic, user, use_shakemap, rupture_file, station_data_file, download_usgs_stations, monitor) if err: return None, None, None, err diff --git a/openquake/hazardlib/tests/shakemap/parsers_test.py b/openquake/hazardlib/tests/shakemap/parsers_test.py index eca361d05a6..537275e4d73 100644 --- a/openquake/hazardlib/tests/shakemap/parsers_test.py +++ b/openquake/hazardlib/tests/shakemap/parsers_test.py @@ -41,25 +41,25 @@ def test_utc_to_local_time(self): def test_1(self): # wrong usgs_id _rup, _rupdic, err = get_rup_dic( - {'usgs_id': 'usp0001cc'}, User(level=2, testdir=''), - 'use_shakemap_from_usgs', use_shakemap=True) + {'usgs_id': 'usp0001cc', 'approach': 'use_shakemap_from_usgs'}, + User(level=2, testdir=''), use_shakemap=True) self.assertIn('Unable to download from https://earthquake.usgs.gov/fdsnws/' 'event/1/query?eventid=usp0001cc&', err['error_msg']) def test_2(self): _rup, dic, _err = get_rup_dic( - {'usgs_id': 'usp0001ccb'}, user=user, approach='use_shakemap_from_usgs', - use_shakemap=True) + {'usgs_id': 'usp0001ccb', 'approach': 'use_shakemap_from_usgs'}, + user=user, use_shakemap=True) self.assertIsNotNone(dic['shakemap_array']) _rup, dic, _err = get_rup_dic( - {'usgs_id': 'usp0001ccb'}, user=user, approach='use_shakemap_from_usgs', - use_shakemap=False) + {'usgs_id': 'usp0001ccb', 'approach': 'use_shakemap_from_usgs'}, + user=user, use_shakemap=False) self.assertIsNone(dic['shakemap_array']) def test_3(self): _rup, dic, _err = get_rup_dic( - {'usgs_id': 'us6000f65h'}, user=user, approach='use_pnt_rup_from_usgs', - use_shakemap=True) + {'usgs_id': 'us6000f65h', 'approach': 'use_pnt_rup_from_usgs'}, + user=user, use_shakemap=True) self.assertEqual(dic['lon'], -73.4822) self.assertEqual(dic['lat'], 18.4335) self.assertEqual(dic['dep'], 10.0) @@ -78,8 +78,8 @@ def test_3(self): def test_4(self): # point_rup _rup, dic, _err = get_rup_dic( - {'usgs_id': 'us6000jllz'}, user=user, approach='use_shakemap_from_usgs', - use_shakemap=True) + {'usgs_id': 'us6000jllz', 'approach': 'use_shakemap_from_usgs'}, + user=user, use_shakemap=True) self.assertEqual(dic['lon'], 37.0143) self.assertEqual(dic['lat'], 37.2256) self.assertEqual(dic['dep'], 10.) @@ -88,8 +88,8 @@ def test_4(self): def test_5(self): # 12 vertices instead of 4 in rupture.json rup, dic, _err = get_rup_dic( - {'usgs_id': 'us20002926'}, user=user, approach='use_shakemap_from_usgs', - use_shakemap=True) + {'usgs_id': 'us20002926', 'approach': 'use_shakemap_from_usgs'}, + user=user, use_shakemap=True) self.assertIsNone(rup) self.assertEqual(dic['require_dip_strike'], True) self.assertEqual(dic['rupture_issue'], @@ -97,8 +97,8 @@ def test_5(self): def test_6(self): _rup, dic, _err = get_rup_dic( - {'usgs_id': 'usp0001ccb'}, user=user, approach='use_pnt_rup_from_usgs', - use_shakemap=True) + {'usgs_id': 'usp0001ccb', 'approach': 'use_pnt_rup_from_usgs'}, + user=user, use_shakemap=True) self.assertEqual(dic['mag'], 6.7) self.assertEqual(dic['require_dip_strike'], True) self.assertEqual(dic['station_data_issue'], @@ -107,9 +107,8 @@ def test_6(self): def test_7(self): dic_in = {'usgs_id': 'us6000jllz', 'lon': None, 'lat': None, 'dep': None, 'mag': None, 'msr': '', 'aspect_ratio': 2, 'rake': None, - 'dip': None, 'strike': None} - _rup, dic, _err = get_rup_dic( - dic_in, user=user, approach='build_rup_from_usgs', use_shakemap=True) + 'dip': None, 'strike': None, 'approach': 'build_rup_from_usgs'} + _rup, dic, _err = get_rup_dic(dic_in, user=user, use_shakemap=True) self.assertEqual( dic['nodal_planes'], {'NP1': {'dip': 88.71, 'rake': -179.18, 'strike': 317.63}, @@ -117,9 +116,9 @@ def test_7(self): def test_8(self): dic_in = {'usgs_id': 'us6000jllz', 'lon': 37.0143, 'lat': 37.2256, - 'dep': 10.0, 'mag': 7.8, 'rake': 0.0} - rup, dic, _err = get_rup_dic( - dic_in, user=user, approach='use_pnt_rup_from_usgs', use_shakemap=True) + 'dep': 10.0, 'mag': 7.8, 'rake': 0.0, + 'approach': 'use_pnt_rup_from_usgs'} + rup, dic, _err = get_rup_dic(dic_in, user=user, use_shakemap=True) self.assertEqual(dic['msr'], 'PointMSR') self.assertAlmostEqual(rup.surface.length, 0.0133224) self.assertAlmostEqual(rup.surface.width, 0.0070800) @@ -127,9 +126,9 @@ def test_8(self): def test_9(self): dic_in = {'usgs_id': 'us6000jllz', 'lon': 37.0143, 'lat': 37.2256, 'dep': 10, 'mag': 7.8, 'msr': 'WC1994', 'aspect_ratio': 3, - 'rake': -179.18, 'dip': 88.71, 'strike': 317.63} - _rup, dic, _err = get_rup_dic( - dic_in, user=user, approach='build_rup_from_usgs', use_shakemap=True) + 'rake': -179.18, 'dip': 88.71, 'strike': 317.63, + 'approach': 'build_rup_from_usgs'} + _rup, dic, _err = get_rup_dic(dic_in, user=user, use_shakemap=True) self.assertEqual(dic['dep'], 10) self.assertEqual(dic['dip'], 88.71) self.assertEqual(dic['lat'], 37.2256) @@ -144,9 +143,19 @@ def test_9(self): def test_10(self): dic_in = {'usgs_id': 'us6000jllz', 'lon': 37.0143, 'lat': 37.2256, 'dep': 10.0, 'mag': 7.8, 'msr': 'WC1994', 'aspect_ratio': 2.0, - 'rake': -179.18, 'dip': 88.71, 'strike': 317.63} + 'rake': -179.18, 'dip': 88.71, 'strike': 317.63, + 'approach': 'build_rup_from_usgs'} + _rup, _dic, err = get_rup_dic( + dic_in, user=user, use_shakemap=True) + self.assertIn('The depth must be greater', err['error_msg']) + + def test_11(self): + dic_in = {'usgs_id': 'UserProvided', 'lon': -9, 'lat': 43, 'dep': 10, + 'mag': 8.5, 'msr': 'WC1994', 'aspect_ratio': 1, + 'rake': 90, 'dip': 90, 'strike': 0, + 'approach': 'provide_rup_params'} _rup, _dic, err = get_rup_dic( - dic_in, user=user, approach='build_rup_from_usgs', use_shakemap=True) + dic_in, user=user, use_shakemap=False) self.assertIn('The depth must be greater', err['error_msg']) From f88666794c40241305f69895c814efab327eb015 Mon Sep 17 00:00:00 2001 From: Paolo Tormene Date: Mon, 24 Feb 2025 14:39:09 +0100 Subject: [PATCH 6/6] Produce and upload demos zip file also for engine-3.23 --- .github/workflows/demos_ltr_latest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/demos_ltr_latest.yml b/.github/workflows/demos_ltr_latest.yml index f3915e758ba..31a252d35a6 100644 --- a/.github/workflows/demos_ltr_latest.yml +++ b/.github/workflows/demos_ltr_latest.yml @@ -9,7 +9,7 @@ on: required: true push: # TODO: it would be better to point to ltr and latest - branches: [ engine-3.16, engine-3.17, engine-3.18, engine-3.19, engine-3.20, engine-3.21, engine-3.22 ] + branches: [ engine-3.16, engine-3.17, engine-3.18, engine-3.19, engine-3.20, engine-3.21, engine-3.22, engine-3.23 ] jobs: demos: runs-on: ubuntu-latest