2024,江端さんの技術メモ

/*
	c:\users\ebata\dscan\dscan3.go

	DBSCANアルゴリズムを実装し、3次元空間のデータ(x、y、t)をクラスタリングするシンプルなプログラムです。
	このプログラムでは、各データポイントは3次元座標(x、y、t)で表され、距離の計算にはユークリッド距離を使用します。

	DBSCANをk-meansの違いで説明します。

	DBSCAN(Density-Based Spatial Clustering of Applications with Noise)とk-meansは、クラスタリングアルゴリズムですが、そのアプローチや動作原理にはいくつかの違いがあります。

	(1)クラスタリング方法:

	- DBSCAN: 密度ベースのクラスタリングアルゴリズムであり、データポイントの密度に基づいてクラスタを形成します。各点は、一定の距離(ε、epsilon)内に最小限の近傍点数(minPts)が存在する場合、その点を中心としたクラスタが形成されます。
	- k-means: 距離ベースのクラスタリングアルゴリズムであり、データポイントの距離に基づいてクラスタを形成します。クラスタの数(k)を事前に指定し、各点を最も近いセントロイド(クラスタの中心)に割り当てます。

	(2)クラスタの形状:

	- DBSCAN: クラスタの形状は任意であり、密度の高い領域に基づいて形成されます。したがって、DBSCANは非凸形状のクラスタを処理できます。
	- k-means: クラスタの形状は円形(球形)であり、各クラスタのセントロイドからの距離に基づいて決定されます。したがって、k-meansは凸形状のクラスタを前提としています。

	(3)ハイパーパラメータ:

	- DBSCAN: ε(epsilon)とminPtsの2つのハイパーパラメータを必要とします。εは近傍点の距離の閾値を定義し、minPtsはクラスタと見なすための最小の近傍点数を指定します
	- k-means: クラスタの数(k)を指定する必要があります。

	(4)ノイズの処理:

	- DBSCAN: ノイズポイントを自動的に検出し、外れ値として扱います。密度が低い領域に存在するポイントは、任意のクラスタに割り当てられず、ノイズとして扱われます。
	-k-means: 外れ値やノイズの処理を明示的に行いません。各点は必ずどれかのクラスタに割り当てられます。

	 要するに、DBSCANは密度ベースのアルゴリズムであり、任意の形状のクラスタを検出し、ノイズを処理する能力があります。一方、k-meansは距離ベースのアルゴリズムであり、クラスタの形状が円形であることを前提としています。


*/

package main

import (
	"fmt"
	"math"
)

// Point represents a 3D point with coordinates x, y, and t
type Point struct {
	X, Y, T float64
}

// DistanceTo calculates the Euclidean distance between two 3D points
func (p Point) DistanceTo(other Point) float64 {
	dx := p.X - other.X
	dy := p.Y - other.Y
	dt := p.T - other.T
	return math.Sqrt(dx*dx + dy*dy + dt*dt)
}

// Cluster represents a cluster of points
type Cluster struct {
	Points []Point
}

// DBSCAN performs density-based clustering of 3D points
func DBSCAN(points []Point, epsilon float64, minPts int) []Cluster {
	var clusters []Cluster
	var visited = make(map[Point]bool)

	for _, point := range points {
		if visited[point] {
			continue
		}
		visited[point] = true

		neighbours := getNeighbours(points, point, epsilon)
		if len(neighbours) < minPts {
			continue
		}

		var clusterPoints []Point
		expandCluster(&clusterPoints, points, visited, point, neighbours, epsilon, minPts)
		clusters = append(clusters, Cluster{Points: clusterPoints})
	}

	return clusters
}

// getNeighbours returns all points within distance epsilon of the given point
func getNeighbours(points []Point, point Point, epsilon float64) []Point {
	var neighbours []Point
	for _, other := range points {
		if point.DistanceTo(other) <= epsilon {
			neighbours = append(neighbours, other)
		}
	}
	return neighbours
}

// expandCluster expands the cluster from the given point
func expandCluster(cluster *[]Point, points []Point, visited map[Point]bool, point Point, neighbours []Point, epsilon float64, minPts int) {
	*cluster = append(*cluster, point)
	for _, neighbour := range neighbours {
		if !visited[neighbour] {
			visited[neighbour] = true
			neighbourNeighbours := getNeighbours(points, neighbour, epsilon)
			if len(neighbourNeighbours) >= minPts {
				expandCluster(cluster, points, visited, neighbour, neighbourNeighbours, epsilon, minPts)
			}
		}
		// Add neighbour to cluster if not already in another cluster
		var isInCluster bool
		for _, c := range *cluster {
			if c == neighbour {
				isInCluster = true
				break
			}
		}
		if !isInCluster {
			*cluster = append(*cluster, neighbour)
		}
	}
}

func main() {
	// Example usage
	points := []Point{
		{X: 1, Y: 2, T: 0},
		{X: 1.5, Y: 1.8, T: 1},
		{X: 5, Y: 8, T: 2},
		{X: 8, Y: 8, T: 3},
		{X: 1, Y: 0.6, T: 4},
		{X: 9, Y: 11, T: 5},
		{X: 8, Y: 2, T: 6},
		{X: 10, Y: 2, T: 7},
		{X: 9, Y: 3, T: 8},
	}

	epsilon := 3.0
	minPts := 2

	clusters := DBSCAN(points, epsilon, minPts)
	for i, cluster := range clusters {
		fmt.Printf("Cluster %d:\n", i+1)
		for _, point := range cluster.Points {
			fmt.Printf("  (%.2f, %.2f, %.2f)\n", point.X, point.Y, point.T)
		}
	}
}

2024,江端さんの技術メモ

を起動したら、

となることがあるので、この対応方法をメモしておきます。

https://app.mindmanager.com/ と入力(#my-filesはつけない)

ファイルが表示されるので、これを選択。

表示されるようになる。


のプロパティを開いて新しいURLをコピペする。

以上

2024,江端さんの技術メモ

本プログラムは、

Webサーバに繋っているブラウザが、全部いなくなったことを確認するプログラム

を拡張したものです。index.htmlは、このページに記載されているものを、そのまま、使って下さい。

こちらのプログラムでは、Webサーバに繋っているブラウザが、全部いなくなったと確認できた場合(30秒後)に、url = "http://localhost:8000/cancelAllVideoStreamGround"へメッセージを渡すプログラムです。

デバッグの為に使っていたコメントが残っていますので、適当に削除して下さい。

# c:\users\ebata\webMonitor.py
# https://wp.kobore.net/%e6%b1%9f%e7%ab%af%e3%81%95%e3%82%93%e3%81%ae%e6%8a%80%e8%a1%93%e3%83%a1%e3%83%a2/post-12475/を参照のこと

import threading
import time
import requests
import sys
from flask import Flask, request, jsonify
from requests.exceptions import Timeout
from flask_cors import CORS


app = Flask(__name__)

CORS(app)  # すべてのリクエストに対してCORSを有効にする

last_heartbeat_time = 0
lock = threading.Lock()

def send_notification():
    url = "http://localhost:8000/cancelAllVideoStreamGround"
    try:
        response = requests.post(url, timeout=3)
        response.raise_for_status()
        print("Notification sent successfully.")
        sys.stdout.flush()  # 標準出力をフラッシュする
    except Timeout:
        print("Error: Timeout while sending notification")
    except requests.exceptions.RequestException as e:
        print(f"Error sending notification: {e}")

def check_heartbeat():
    global last_heartbeat_time
    while True:
        current_time = time.time()
        print("ct:",current_time)
        print("lht:",last_heartbeat_time)
        diff = current_time - last_heartbeat_time
        print("diff:",diff)
        with lock:
            if current_time - last_heartbeat_time > 30:
                send_notification()
                last_heartbeat_time = current_time
        time.sleep(1)

@app.route('/heartbeat', methods=['POST'])
def receive_heartbeat():
    print("pass receive_heartbeat()")
    global last_heartbeat_time
    data = request.get_json()
    print(f"Received heartbeat: {data}")
    sys.stdout.flush()  # 標準出力をフラッシュする
    with lock:
        last_heartbeat_time = time.time()
        print("lht_2:",last_heartbeat_time)
    # data = request.get_json()
    # print(f"Received heartbeat: {data}")
    # sys.stdout.flush()  # 標準出力をフラッシュする
    return jsonify({"status": "OK"})

if __name__ == "__main__":
    heartbeat_thread = threading.Thread(target=check_heartbeat)
    heartbeat_thread.start()
    app.run(host='0.0.0.0', port=3000)

私用のコメント
(1)receive_heartbeat():と receive_heartbeat()を同時に走らせる為に、スレッド化しなければならなくなった → で Ctrl-Cで止められなくなったので、ちょっと困っている(が、シェル画面ごと落せばいいので、現在はそうしている)

(2)url = "http://localhost:8000/cancelAllVideoStreamGround" からの時間がもたつく場合には、Timeoutを設定すると良いかもしれない。send_notification()を丸ごと差し替えると、こんな感じ。

def send_notification():
    url = "http://localhost:8000/cancelAllVideoStreamGround"
    try:
        response = requests.post(url, timeout=3)
        response.raise_for_status()
        print("Notification sent successfully.")
        sys.stdout.flush()  # 標準出力をフラッシュする
    except Timeout:
        print("Error: Timeout while sending notification")
    except requests.exceptions.RequestException as e:
        print(f"Error sending notification: {e}")

(3)Webブラウザは、バックエンドに置いておくと、Heatbeatを出力しないことがあるので、常にアクティブにしておく必要があります(バックエンドのままにしておくと、cancelAllVideoStreamGroundが動き出します)

以上

2024,江端さんの技術メモ

「まだChatGPTを使ってない人は『人生を悔い改めた方がいい』」 ―― と言った、ソフトバンクの孫社長に申し上げます。『いらんこと言うな』と。

にも書いていますが、

『当初、私は、ChatGPT(対話型AIアシスタント))、Grammerly(英語文章構成サービス), Deepl(翻訳サービス)を、無料で使ってきたのですが、私のそのサービスの利用頻度は、無料の範囲を越えてしまい、全て有料の会員となっています』

で、ここにAmazon Lightsailがあって、さらに、さくらインターネットのサービス(kobore.net)が入って、当然、ドメイン名の使用料も含まれていて、もう、これは、IT/AI搾取と称呼してもしても良いのではないかと思います。

で、先程、GitHub Copilot(10ドル/月)にも入りました ―― 貢いでいる対象を考えると「ホスト/ホステスに貢いだ方が楽しそう」です。金額の規模感は違いますが。

ジュニアに質問しにくいシニアにとっては、十分ペイする「お助けサービス」ではあるのですが ―― 私の人生、ハレがない とは思います。

それはさておき


先程まで、GitHub Copilotをvscodeにアドインしたが、tabキーを押しても提案を採用できないという問題に悩まされていました。

参考にさせて頂いたのは、こちら(https://mindtech.jp/?p=2330)のページです。

上記の"承諾する Tab"の部分をマウスでクリックすれば、確定はできるのですが、そんなコーディング作業、かったるくてやっていられません。

私の方も、やはり、Awesome Emacs Keymap と、vscode-emacs-indent が悪さをしていたようです。

これらを無効にした後、vscodeを再立ち上げしたら、コード確定ができるようにはなりましたが、当然ですが、emacsのキーバインドが使えなくなりました。しかし、私にはEmacsの環境が使えないコーディング環境は耐えられそうにありません(体が矯正不能)。

で、ちょっと試しに、Awesome Emacs Keymap を無効して、vscodeを再立ち上げして、GitHub Copilotを起動している状態で、Awesome Emacs Keymap を再び有効にしてみたら、両立に成功しているようです(すぐにボロが出るかもしれませんが)。

とりあえず、メモとして残しおきます。

またコケたら、こちらのメモに追記します。

2024,江端さんの技術メモ

ChatGPTと相談しながら、fastapiの起動時にコマンドを起動する方法を試していたのですが、上手く動かない(最初のコマンドでロックしてしまう)ので、起動したまま、そのまま放置するコード(プロセスを落したければ、コマンドから自力でkillコマンドで落す必要ある)にしました。

import subprocess
from fastapi import FastAPI

app = FastAPI()

# 各コマンドを実行する関数
def run_command(command):
    try:
        # バックグラウンドでプロセスを実行
        subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
    except Exception as e:
        print(f"コマンド '{command}' の実行中にエラーが発生しました: {str(e)}")

# 各コマンドを実行
commands = [
    "sudo socat udp-listen:38089,reuseaddr,fork udp:192.168.3.153:38089",
    "sudo socat udp-listen:38090,reuseaddr,fork udp:192.168.3.153:38090",
    "sudo socat udp-listen:38091,reuseaddr,fork udp:192.168.3.153:38091"
]

for command in commands:
    run_command(command)

@app.on_event("startup")
async def startup_event():
    print("Application startup complete.")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

 

2024,江端さんの技術メモ

import asyncio
from fastapi import FastAPI

app = FastAPI()

async def hello_timer():
    while True:
        print("hello")
        await asyncio.sleep(10)  # 10秒待機

@app.on_event("startup")
async def startup_event():
    # FastAPIアプリケーションの起動時にタイマーを開始
    asyncio.create_task(hello_timer())

@app.get("/")
async def get_hello():
    return {"message": "hello"}

2024,江端さんの技術メモ

/*
C:\Users\ebata\yamaguchi\src_light

■このサンプルプログラムのキモ
(1)座標を入力して一番近いpostGISのノードIDを検知して、ダイクストラ計算を行うこと
(2)ユーザ心理を計算するファジィ推論を行うこと
が入っているプログラムである。

■自動計算を実施するバッチ、dummy.batの内容は以下の通り
go run light-sim.go data/mod_20220522holyday_visitor.csv data/new_20220522holyday_visitor.csv
go run light-sim.go data/mod_20220521holyday_visitor.csv data/new_20220521holyday_visitor.csv
go run light-sim.go data/mod_20220522holyday.csv data/new_20220522holyday.csv
go run light-sim.go data/mod_20220518weekday.csv data/new_20220518weekday.csv

■その入力用のcsvファイルの一つである、"data/mod_20220522holyday_visitor.csv"の内容は以下の通り
34.172891,131.456346,34.182418,131.474987,21
34.163801,131.444142,34.164454,131.449142,62
34.158856,131.435881,34.164727,131.431189,52
(中略)
34.146351,131.461154,34.167045,131.448468,20
34.145639,131.449237,34.149603,131.432828,29

■"data/bike_stations.csv"の中身
index,station,address,lat,lon,initial_stock,max_stock
1,null,null,34.102543,131.392639,20,20
2,null,null,34.102543,131.392639,20,20
3,null,null,34.102543,131.392639,20,20
4,山口県庁前バス停,山口市春日町2086-4,34.183621,131.471688,20,20
5,null,null,34.102543,131.392639,20,20
6,山口市役所 駐輪場,山口市亀山町2-1,34.178056,131.474204,20,20
7,一の坂川交通交流広場,山口市中河原7-1,34.17960577,131.4783006,20,20
8,null,null,34.102543,131.392639,20,20
9,コープやまぐちこことどうもん店 駐輪場,山口市道場門前1-1-18,34.174234,131.474885,20,20
10,山口駅 駐輪場,山口市惣太夫町288-9,34.172219,131.480013,20,20
11,山口市教育委員会 駐輪場,山口市中央5-14-22,34.170346,131.46915,20,20
12,null,null,34.102543,131.392639,20,20
13,ファミリーマート山口泉都町店,山口市泉都町9-2,34.167346,131.462048,20,20
14,防長苑,山口市熊野町4-29,34.167204,131.45973,20,20
15,null,null,34.102543,131.392639,20,20
16,ホテルニュータナカ,山口市湯田温泉2-6-24,34.163937,131.456115,20,20
17,null,null,34.102543,131.392639,20,20
18,湯田温泉駅 駐輪場,山口市今井町146-6,34.159954,131.459967,20,20
19,アルク平川店,山口市平井724-1,34.152742,131.464199,20,20
20,山口大学(正門),山口市吉田1677-1,34.149852,131.466214,20,20
21,小郡総合支所 駐輪場,山口市小郡下郷609番地1,34.102543,131.392639,20,20
22,KDDI維新ホール 駐輪場,山口市小郡令和1丁目1番地,34.09368,131.394,20,20
23,風の並木通り(新山口駅南口側),山口市小郡金町1-1付近,34.092322,131.397667,20,20
24,平成公園 駐車場内,山口市小郡平成町3-1,34.088168,131.401698,20,20
25,null,null,34.102543,131.392639,20,20
26,アルク小郡店,山口市小郡下郷2273番地1,34.097135,131.391295,20,20
27,null,null,34.102543,131.392639,20,20


*/
package main

import (
	"database/sql"
	"encoding/csv"
	"fmt"
	"log"

	"os"
	"strconv"

	_ "github.com/lib/pq"
)

// 自転車の型
type BikeParam struct {
	id            int // 自転車の識別番号
	destinationId int // 出発座標番号(*1)
	arrivalId     int // 到着座標番号

	// (*1)
	// 名前がdestination(目的地)となっているのは、バスがユーザを迎えに行き、載せた時点が「出発」扱いであった名残。
	// 自転車の場合は迎えに行く動作が無いので、名称変更が望ましい。

}

// 座標情報
type LocInfo struct {
	Lng    float64 // 経度
	Lat    float64 // 緯度
	Source int     // 地図DBのID
}

// 自転車の拠点
type BikeStation struct {
	location     LocInfo // 拠点の位置
	initialStock int     // 最初に配置する自転車の台数
	stationName  string  // 拠点の名称
}

// ステーションからの自転車の出入り
type StationStock struct {
	outgoing int
	incoming int
}

var stationstock [40]StationStock

const STATIONS_PATH string = "data/bike_stations.csv"

// 拠点情報を読み込み、BikeStation型の配列を返す
func getStationInfo(dbMap *sql.DB) []BikeStation {
	// ファイルをオープン
	csvFile, err := os.Open(STATIONS_PATH)
	if err != nil {
		log.Fatal(err)
	}
	defer csvFile.Close()

	// CSVファイルの中身を読み込み
	r := csv.NewReader(csvFile)
	rows, err := r.ReadAll()
	if err != nil {
		log.Fatal(err)
	}

	stationInfo := []BikeStation{} // 出力変数

	// 行ごとに
	for i, row := range rows {
		if i == 0 {
			continue // CSVのヘッダー行を無視
		}
		lat, err := strconv.ParseFloat(row[3], 64)
		if err != nil {
			log.Fatal(err)
		}
		lng, err := strconv.ParseFloat(row[4], 64)
		if err != nil {
			log.Fatal(err)
		}
		initialStockVal, err := strconv.Atoi(row[5])
		if err != nil {
			log.Fatal(err)
		}
		stationNameVal := row[1]

		// CSV読み込み時点の座標のログ(地図DBによって補正する前の座標)
		//log.Println("csv read result:", lng, lat, initialStock)

		// 地図DBを参照することによって、ステーションの位置をノードの位置にする
		source, lngModified, latModified := fixPosition(dbMap, lng, lat)
		loc := LocInfo{
			Lng:    lngModified,
			Lat:    latModified,
			Source: source,
		}

		// 読み込めていることの確認ログ
		//log.Println("Station", (i - 1), lngModified, latModified, initialStock, source)

		bikeStation := BikeStation{
			location:     loc,
			initialStock: initialStockVal,
			stationName:  stationNameVal,
		}
		stationInfo = append(stationInfo, bikeStation)
	}
	return stationInfo
}

// Scan用の仮変数
var source int
var longitude float64
var latitude float64
var dist float64

// 指定した座標に近いDB上の座標を取得
func fixPosition(db *sql.DB, _x1, _y1 float64) (int, float64, float64) {

	upperLimitMeter := 1500.0 // 近傍ノードの上限を1500 mに設定
	str := fmt.Sprintf(
		// 修正前: ways (道) の中から最近傍を取得
		// "SELECT source, x1 AS longitude, y1 AS latitude, ST_Distance('SRID=4326;POINT(%v %v)'::GEOGRAPHY, the_geom) AS dist FROM ways WHERE ST_DWithin(the_geom, ST_GeographyFromText('SRID=4326;POINT(%v %v)'), %.1f) ORDER BY dist LIMIT 1",
		// 修正後: ways_vertices_pgr (点座標) の中から最近傍を取得
		"SELECT id AS source, lon AS longitude, lat AS latitude, ST_Distance('SRID=4326;POINT(%v %v)'::GEOGRAPHY, the_geom) AS dist FROM ways_vertices_pgr WHERE ST_DWithin(the_geom, ST_GeographyFromText('SRID=4326;POINT(%v %v)'), %.1f) ORDER BY dist LIMIT 1",
		_x1, _y1, _x1, _y1, upperLimitMeter,
	)

	//fmt.Println(str)

	rows, err := db.Query(str)
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()

	foundGoodMapNode := false

	for rows.Next() {
		foundGoodMapNode = true
		if err := rows.Scan(&source, &longitude, &latitude, &dist); err != nil {
			fmt.Println(err)
		}
		//fmt.Println(source, longitude, latitude, dist)
	}

	if !foundGoodMapNode {
		log.Println("Warning: in func fixPosition: Good Map Node not found for query point (",
			_x1, ",", _y1, ")")
	}

	return source, longitude, latitude
}

/*
func getShortestDistanceToBikeStation(dbMap *sql.DB, node int) int {

	StationId := -1
	distance := 1000000.0

	for i := 0; i < 2; i++ {
		rowsDijkstra, errDijkstra := dbMap.Query(
			"SELECT seq,source, target, x1, y1, x2, y2, agg_cost FROM pgr_dijkstra('SELECT gid as id, source, target, length_m as cost FROM ways', $1::bigint , $2::bigint , directed:=false) a INNER JOIN ways b ON (a.edge = b.gid) ORDER BY seq",
			node,

			Station[i].Node)
		if errDijkstra != nil {
			log.Fatal(errDijkstra)
		}
		defer rowsDijkstra.Close()

		var agg_cost float64

		for rowsDijkstra.Next() {
			var x1, y1, x2, y2 float64
			var seq, source, target int

			err := rowsDijkstra.Scan(&seq, &source, &target, &x1, &y1, &x2, &y2, &agg_cost)
			if err != nil {
				fmt.Println(err)
			}
		}

		if distance > agg_cost {
			distance = agg_cost
			StationId = i
		}
	}
	return StationId
}
*/

// 江端修正版
func getDijkstraPath(dbMap *sql.DB, locInfoStart, locInfoGoal LocInfo) ([]LocInfo, float64) {
	//log.Println("getDijkstraPath", locInfoStart, locInfoGoal)

	var path []LocInfo // 経路 (返り値の一つ目)
	var totalDistanceKm float64

	rowsDijkstra, errDijkstra := dbMap.Query(
		"SELECT seq,source, target, x1, y1, x2, y2, agg_cost FROM pgr_dijkstra('SELECT gid as id, source, target, length_m as cost FROM ways', $1::bigint , $2::bigint , directed:=false) a INNER JOIN ways b ON (a.edge = b.gid) ORDER BY seq",
		locInfoStart.Source,
		locInfoGoal.Source)

	if errDijkstra != nil {
		log.Fatal(errDijkstra)
	}
	defer rowsDijkstra.Close()

	var agg_cost float64

	isFirstCheck := true
	isSourceCheck := true

	for rowsDijkstra.Next() {

		var x1, y1, x2, y2 float64

		var seq int
		var target int

		// まずnodeを読む
		if err := rowsDijkstra.Scan(&seq, &source, &target, &x1, &y1, &x2, &y2, &agg_cost); err != nil {
			fmt.Println(err)
		}

		// 最初の1回だけ入る
		if isFirstCheck {
			if source == locInfoStart.Source {
				isSourceCheck = true
			} else {
				isSourceCheck = false
			}
			isFirstCheck = false
		}

		var loc LocInfo

		if isSourceCheck {
			loc.Source = source
			loc.Lng = x1
			loc.Lat = y1
		} else {
			loc.Source = target
			loc.Lng = x2
			loc.Lat = y2
		}

		loc.Source = target

		path = append(path, loc)
	}

	// ラストノードだけは手入力
	path = append(path, locInfoGoal)

	totalDistanceKm = agg_cost / 1000.0
	return path, totalDistanceKm
}

// 一番近いステーションのIDを取得
func getNearestStation(dbMap *sql.DB, stationInfo []BikeStation, queryLocation LocInfo) (int, float64) {
	bestDistanceKm := 1000.0 // 十分に大きい数
	var bestStationId int
	for i := 0; i < len(stationInfo); i++ {

		// ダイクストラ法による経路で決定する距離
		// SQLクエリを繰り返し実行するため、処理が遅くなる可能性がある
		_, distKm := getDijkstraPath(dbMap, queryLocation, stationInfo[i].location)

		// 直線距離の概算値(代替の計算方法)
		//distKm, _ := distanceKm(queryLocation.Lng, queryLocation.Lat,
		//	stationInfo[i].location.Lng, stationInfo[i].location.Lat)

		//log.Println("to station", i, "distanceKm=", distKm)

		if distKm < bestDistanceKm {
			bestDistanceKm = distKm
			bestStationId = i
		}
	}
	return bestStationId, bestDistanceKm
}

func main() {

	dbMap, err := sql.Open("postgres",
		"user=postgres password=password host=192.168.0.23 port=15432 dbname=yama_db sslmode=disable")
	log.Println("------------------ map db open ------------------")
	if err != nil {
		log.Fatal("OpenError: ", err)
	}
	defer dbMap.Close()

	// バイクステーション情報の読み込み
	stationInfo := getStationInfo(dbMap)

	//fmt.Println(stationInfo)

	//file2, err2 := os.Open("data/new_20220518weekday.csv")
	file2, err2 := os.Open(os.Args[1])
	if err2 != nil {
		log.Fatal(err2)
	}
	defer file2.Close()

	r2 := csv.NewReader(file2)
	rows2, err2 := r2.ReadAll() // csvを一度に全て読み込む
	if err != nil {
		log.Fatal(err2)
	}

	//file3, err3 := os.Create("data/calc2_new_20220518weekday.csv") // 第2パラメータ
	file3, err3 := os.Create(os.Args[2]) // 第2パラメータ
	if err3 != nil {
		panic(err)
	}
	w := csv.NewWriter(file3)

	output := []string{"id", "age", "origin_loc_Lng", "origin_loc_Lat", "dest_loc_Lng", "dest_loc_Lat", "distance_from_origin", "distance_between_stations", "distance_to_dest", "complain"}

	if err = w.Write(output); err != nil {
		log.Fatal(err)
	}

	for id, row := range rows2 {

		/*
			origin_lng := 131.4686813247102
			origin_lat := 34.17901518198008

			dest_lng := 131.45836175237153
			dest_lat := 34.160484344205294
		*/

		origin_lng, err := strconv.ParseFloat(row[1], 64)
		if err != nil {
			log.Fatal(err)
		}
		origin_lat, err := strconv.ParseFloat(row[0], 64)
		if err != nil {
			log.Fatal(err)
		}

		dest_lng, err := strconv.ParseFloat(row[3], 64)
		if err != nil {
			log.Fatal(err)
		}

		dest_lat, err := strconv.ParseFloat(row[2], 64)
		if err != nil {
			log.Fatal(err)
		}

		age, err := strconv.ParseFloat(row[4], 64) // 年齢も実数扱いする
		if err != nil {
			log.Fatal(err)
		}

		// 最接近のステーションを選ぶ

		var origin_loc, dest_loc LocInfo

		origin_loc.Source, origin_loc.Lng, origin_loc.Lat = fixPosition(dbMap, origin_lng, origin_lat)
		dest_loc.Source, dest_loc.Lng, dest_loc.Lat = fixPosition(dbMap, dest_lng, dest_lat)

		stationId_from_origin, distance_from_origin := getNearestStation(dbMap, stationInfo, origin_loc) // Originから最初のステーション
		stationId_to_dest, distance_to_dest := getNearestStation(dbMap, stationInfo, dest_loc)           // 最後のステーションからDest

		fmt.Println(stationInfo[stationId_from_origin])
		stationstock[stationId_from_origin].outgoing++

		fmt.Println(stationInfo[stationId_to_dest])
		stationstock[stationId_to_dest].incoming++

		_, distance_between_stations := getDijkstraPath(dbMap, stationInfo[stationId_from_origin].location, stationInfo[stationId_to_dest].location)
		_, shortest_distance_total := getDijkstraPath(dbMap, origin_loc, dest_loc)

		fmt.Println("stationId_from_origin, distance_from_origin", stationId_from_origin, distance_from_origin)
		fmt.Println("stationId_to_dest, distance_to_dest", stationId_to_dest, distance_to_dest)
		fmt.Println("distance_between_stations", distance_between_stations)
		fmt.Println("shortest_distance_total", shortest_distance_total)

		// 出発 ― _x[km]の歩行 ― 最初のステーション ― _y[km]の自転車走行 ― 最後のステーション ― _[km]の歩行 ―  到着
		complain := fuzzy_reasoning(distance_from_origin, distance_between_stations, distance_to_dest, age)
		fmt.Println("complain", complain)

		output := []string{
			fmt.Sprint(id),
			fmt.Sprint(age),
			fmt.Sprint(origin_loc.Lng),
			fmt.Sprint(origin_loc.Lat),
			fmt.Sprint(dest_loc.Lng),
			fmt.Sprint(dest_loc.Lat),
			fmt.Sprint(distance_from_origin),
			fmt.Sprint(distance_between_stations),
			fmt.Sprint(distance_to_dest),
			fmt.Sprint(complain)}

		fmt.Println(output)

		if err = w.Write(output); err != nil {
			log.Fatal(err)
		}
	}

	// test
	output = []string{"stationid", "outgoing", "incoming"}
	if err = w.Write(output); err != nil {
		log.Fatal(err)
	}

	for i := 0; i < 40; i++ {
		output = []string{fmt.Sprint(i + 1), fmt.Sprint(stationstock[i].outgoing), fmt.Sprint(stationstock[i].incoming)}
		// i+1しているのは、1からスタートする為
		if err = w.Write(output); err != nil {
			log.Fatal(err)
		}
	}

	defer w.Flush()

	if err := w.Error(); err != nil {
		log.Fatal(err)
	}
}

func fuzzy_reasoning(distance_from_origin, distance_between_stations, distance_to_dest, age float64) float64 {

	////// パラメータの作成

	// 絶対的歩行距離
	walk := min_2(distance_from_origin, distance_to_dest)
	// 総体的自転車移動距離
	ratio := distance_between_stations / (distance_from_origin + distance_between_stations + distance_to_dest)
	// 絶対的自転車移動距離
	bike := distance_between_stations

	// Age(前件部)
	Age_Less := new_condition_MF3(45, 20, "LESS")
	Age_Common := new_condition_MF3(45, 20, "COMMON") // 中央が 42歳
	Age_More := new_condition_MF3(45, 20, "MORE")

	// 絶対的歩行距離(前件部)
	Walk_Less := new_condition_MF3(0.4, 0.1, "LESS")
	Walk_Common := new_condition_MF3(0.4, 0.1, "COMMON")
	Walk_More := new_condition_MF3(0.4, 0.1, "MORE")

	// 相対的自転車移動比率(former)
	Bike_Ratio_Less := new_condition_MF3(0.7, 0.1, "LESS")
	Bike_Ratio_Common := new_condition_MF3(0.7, 0.1, "COMMON")
	Bike_Ratio_More := new_condition_MF3(0.7, 0.1, "MORE")

	// 絶対的自転車距離(前件部)
	Bike_Less := new_condition_MF3(1.5, 1.0, "LESS")
	Bike_Common := new_condition_MF3(1.5, 1.0, "COMMON")
	Bike_More := new_condition_MF3(1.5, 1.0, "MORE")

	// Complain(後件部)
	Complain_LessLess := new_action_MF5(0.5, 0.25, "LESSLESS")
	Complain_Less := new_action_MF5(0.5, 0.25, "LESS")
	Complain_Common := new_action_MF5(0.5, 0.25, "COMMON")
	Complain_More := new_action_MF5(0.5, 0.25, "MORE")
	Complain_MoreMore := new_action_MF5(0.5, 0.25, "MOREMORE")

	// [Rule A00]
	Rule_A00 := min_2(Age_Less.func_X(age), Walk_Less.func_X(walk))
	Complain_LessLess.func_Max(Rule_A00)
	//fmt.Println("Rule_A00", Rule_A00)

	// [Rule A01]
	Rule_A01 := min_2(Age_Less.func_X(age), Walk_Common.func_X(walk))
	Complain_Less.func_Max(Rule_A01)
	//fmt.Println("Rule_A01", Rule_A01)

	// [Rule A02]
	Rule_A02 := min_2(Age_Less.func_X(age), Walk_More.func_X(walk))
	Complain_Common.func_Max(Rule_A02)
	//fmt.Println("Rule_A02", Rule_A02)

	// [Rule A10]
	Rule_A10 := min_2(Age_Common.func_X(age), Walk_Less.func_X(walk))
	Complain_Common.func_Max(Rule_A10)
	//fmt.Println("Rule_A10", Rule_A10)

	// [Rule A11]
	Rule_A11 := min_2(Age_Common.func_X(age), Walk_Common.func_X(walk))
	Complain_Common.func_Max(Rule_A11)
	//fmt.Println("Rule_A11", Rule_A11)

	// [Rule A12]
	Rule_A12 := min_2(Age_Common.func_X(age), Walk_More.func_X(walk))
	Complain_More.func_Max(Rule_A12)
	//fmt.Println("Rule_A12", Rule_A12)

	// [Rule A20]
	Rule_A20 := min_2(Age_More.func_X(age), Walk_Less.func_X(walk))
	Complain_Common.func_Max(Rule_A20)
	//fmt.Println("Rule_A20", Rule_A20)

	// [Rule A21]
	Rule_A21 := min_2(Age_More.func_X(age), Walk_Common.func_X(walk))
	Complain_More.func_Max(Rule_A21)
	//fmt.Println("Rule_A21", Rule_A21)

	// [Rule A22]
	Rule_A22 := min_2(Age_More.func_X(age), Walk_More.func_X(walk))
	Complain_MoreMore.func_Max(Rule_A22)
	//fmt.Println("Rule_A22", Rule_A22)

	// [Rule B00]
	Rule_B00 := Bike_Ratio_Less.func_X(ratio)
	Complain_MoreMore.func_Max(Rule_B00)
	//fmt.Println("Rule_B00", Rule_B00)

	// [Rule B01]
	Rule_B01 := Bike_Ratio_Common.func_X(ratio)
	Complain_Common.func_Max(Rule_B01)
	//fmt.Println("Rule_B01", Rule_B01)

	// [Rule B02]
	Rule_B02 := Bike_Ratio_More.func_X(ratio)
	Complain_LessLess.func_Max(Rule_B02)
	//fmt.Println("Rule_B02", Rule_B02)

	// [Rule C00]
	Rule_C00 := min_2(Age_Less.func_X(age), Bike_Less.func_X(bike))
	Complain_LessLess.func_Max(Rule_C00)
	//fmt.Println("Rule_C00", Rule_C00)

	// [Rule C01]
	Rule_C01 := min_2(Age_Less.func_X(age), Bike_Common.func_X(bike))
	Complain_LessLess.func_Max(Rule_C01)
	//fmt.Println("Rule_C01", Rule_C01)

	// [Rule C02]
	Rule_C02 := min_2(Age_Less.func_X(age), Bike_More.func_X(bike))
	Complain_LessLess.func_Max(Rule_C02)
	//fmt.Println("Rule_C02", Rule_C02)

	// [Rule C10]
	Rule_C10 := min_2(Age_Common.func_X(age), Bike_Less.func_X(bike))
	Complain_Less.func_Max(Rule_C10)
	//fmt.Println("Rule_C10", Rule_C10)

	// [Rule C11]
	Rule_C11 := min_2(Age_Common.func_X(age), Bike_Common.func_X(bike))
	Complain_Common.func_Max(Rule_C11)
	//fmt.Println("Rule_C11", Rule_C11)

	// [Rule C12]
	Rule_C12 := min_2(Age_Common.func_X(age), Bike_More.func_X(bike))
	Complain_More.func_Max(Rule_C12)
	//fmt.Println("Rule_C12", Rule_C12)

	// [Rule C20]
	Rule_C20 := min_2(Age_More.func_X(age), Bike_Less.func_X(bike))
	Complain_Common.func_Max(Rule_C20)
	//fmt.Println("Rule_C20", Rule_C20)

	// [Rule C21]
	Rule_C21 := min_2(Age_More.func_X(age), Bike_Common.func_X(bike))
	Complain_More.func_Max(Rule_C21)
	//fmt.Println("Rule_C21", Rule_C21)

	// [Rule C22]
	Rule_C22 := min_2(Age_More.func_X(age), Bike_More.func_X(bike))
	Complain_MoreMore.func_Max(Rule_C22)
	//fmt.Println("Rule_C22", Rule_C22)

	// Reasoning calculations
	numerator :=
		Complain_LessLess.func_X()*Complain_LessLess.func_Y() +
			Complain_Less.func_X()*Complain_Less.func_Y() +
			Complain_Common.func_X()*Complain_Common.func_Y() +
			Complain_More.func_X()*Complain_More.func_Y() +
			Complain_MoreMore.func_X()*Complain_MoreMore.func_Y()

	denominator :=
		Complain_LessLess.func_Y() +
			Complain_Less.func_Y() +
			Complain_Common.func_Y() +
			Complain_More.func_Y() +
			Complain_MoreMore.func_Y()

	reasoning := numerator / denominator

	return reasoning

}

func max_2(a, b float64) float64 {
	if a > b {
		return a
	} else {
		return b
	}
}

func min_2(a, b float64) float64 {
	if a > b {
		return b
	} else {
		return a
	}
}

type condition_MF3 struct { // Base class for condition_MF3
	center  float64
	width   float64
	express string
}

func new_condition_MF3(_center, _width float64, _express string) *condition_MF3 {
	c3 := new(condition_MF3)
	c3.center = _center
	c3.width = _width
	c3.express = _express
	return c3
}

// Class for the membership function (3 mountains) of the former case
func (c3 *condition_MF3) func_X(_x float64) float64 {
	// x,y denote coordinates on the membership function
	x := _x
	y := 0.0 // The value of y is always greater than or equal to 0 and less than or equal to 1

	if c3.express == "LESS" {
		if x <= c3.center-c3.width {
			y = 1.0
		} else if x <= c3.center {
			y = -1.0 / c3.width * (x - c3.center)
		} else {
			y = 0.0
		}
	} else if c3.express == "COMMON" {
		if x <= c3.center-c3.width {
			y = 0.0
		} else if x <= c3.center {
			y = 1.0/c3.width*(x-c3.center) + 1.0
		} else if x <= c3.center+c3.width {
			y = -1.0/c3.width*(x-c3.center) + 1.0
		} else {
			y = 0.0
		}
	} else if c3.express == "MORE" {
		if x <= c3.center {
			y = 0.0
		} else if x <= c3.center+c3.width {
			y = 1.0 / c3.width * (x - c3.center)
		} else {
			y = 1.0
		}
	} else {
		fmt.Println("MF3: wrong expression")
		os.Exit(1)
	}
	return y
}

type condition_MF5 struct { // Base class for condition_MF5
	center  float64
	width   float64
	express string
}

func new_condition_MF5(_center, _width float64, _express string) *condition_MF5 {
	c5 := new(condition_MF5)
	c5.center = _center
	c5.width = _width
	c5.express = _express
	return c5
}

func (c5 *condition_MF5) func_X(_x float64) float64 {
	// Class for the former membership function (5 mountains)
	// x,y are the coordinates on the membership function

	x := _x
	y := 0.0 // The value of y is always greater than or equal to 0 and less than or equal to 1

	if c5.express == "LESSLESS" {
		if x <= c5.center-2.0*c5.width {
			y = 1.0
		} else if x <= c5.center-c5.width {
			y = -1.0/c5.width*(x-(c5.center-2.0*c5.width)) + 1.0
		} else {
			y = 0.0
		}
	} else if c5.express == "LESS" {
		if x <= c5.center-2.0*c5.width {
			y = 0.0
		} else if x <= c5.center-c5.width {
			y = 1.0/c5.width*(x-(c5.center-c5.width)) + 1.0
		} else if x <= c5.center {
			y = -1.0/c5.width*(x-(c5.center-c5.width)) + 1.0
		} else {
			y = 0.0
		}
	} else if c5.express == "COMMON" {
		if x <= c5.center-c5.width {
			y = 0.0
		} else if x <= c5.center {
			y = 1.0/c5.width*(x-c5.center) + 1.0
		} else if x <= c5.center+c5.width {
			y = -1.0/c5.width*(x-c5.center) + 1.0
		} else {
			y = 0.0
		}
	} else if c5.express == "MORE" {
		if x <= c5.center {
			y = 0.0
		} else if x <= c5.center+c5.width {
			y = 1.0/c5.width*(x-(c5.center+c5.width)) + 1.0
		} else if x <= c5.center+2.0*c5.width {
			y = -1.0/c5.width*(x-(c5.center+c5.width)) + 1.0
		} else {
			y = 0.0
		}
	} else if c5.express == "MOREMORE" {
		if x <= c5.center+c5.width {
			y = 0.0
		} else if x <= c5.center+2.0*c5.width {
			y = 1.0/c5.width*(x-(c5.center+2.0*c5.width)) + 1.0
		} else {
			y = 1.0
		}
	} else {
		fmt.Println("MF5 func_X(): wrong expression")
		os.Exit(1)
	}

	return y
}

/////////////////////////////

type action_MF5 struct { // Base class for action_MF5
	center  float64
	width   float64
	express string
	x       float64
	y       float64
}

type action_MF3 struct { // Base class for action_MF3
	center  float64
	width   float64
	express string
	x       float64
	y       float64
}

func new_action_MF5(_center, _width float64, _express string) *action_MF5 {
	a5 := new(action_MF5)
	a5.center = _center
	a5.width = _width
	a5.express = _express

	if a5.express == "LESSLESS" {
		a5.x = a5.center - 2.0*a5.width
	} else if a5.express == "LESS" {
		a5.x = a5.center - a5.width
	} else if a5.express == "COMMON" {
		a5.x = a5.center
	} else if a5.express == "MORE" {
		a5.x = a5.center + a5.width
	} else if a5.express == "MOREMORE" {
		a5.x = a5.center + 2.0*a5.width
	} else {
		fmt.Println("new_action_MF5: wrong scale expression")
		os.Exit(-1)
	}

	a5.y = 0.0

	return a5
}

func new_action_MF3(_center, _width float64, _express string) *action_MF3 {
	a3 := new(action_MF3)
	a3.center = _center
	a3.width = _width
	a3.express = _express

	if a3.express == "LESS" {
		a3.x = a3.center - a3.width
	} else if a3.express == "COMMON" {
		a3.x = a3.center
	} else if a3.express == "MORE" {
		a3.x = a3.center + a3.width
	} else {
		fmt.Println("new_action_MF3: wrong scale expression")
		os.Exit(-1)
	}

	a3.y = 0.0

	return a3
}

// The latter membership function (5 mountains) class
func (a5 *action_MF5) func_Y() float64 {
	return a5.y
}

// The latter membership function (3 mountains) class
func (a3 *action_MF3) func_Y() float64 {
	return a3.y
}

func (a5 *action_MF5) func_Max(b float64) {
	a5.y = max_2(b, a5.y)
}

func (a3 *action_MF3) func_Max(b float64) {
	a3.y = max_2(b, a3.y)
}

func (a5 *action_MF5) func_X() float64 {
	return a5.x
}

func (a3 *action_MF3) func_X() float64 {
	return a3.x
}

2024,江端さんの忘備録,江端さんの技術メモ

『6ヶ月』 ―― これが、私が計算と経験則から導き出した結論です。

Six months" is the conclusion I have drawn from my calculations and rule of thumb.

上記の私のコラムの5ページ目に、以下の記載があります。

You will find the following statement on page 5 of my column above.

■「この私」が、3・11の震災(東日本大震災)をどのように忘れていったのかを、定量的に知りたいと思いました。

- I wanted to know quantitatively how "this I" had forgotten about the 3/11 disaster (the Great East Japan)

■そこで、私がここ何年間、1日も欠かさずに記録し続けているブログを使って、以下のような調査をやってみました。

- I did the following survey using my blog, which I have kept track of without missing a single day for the past years.

-----

上記の私のコラムの6ページ目に、以下の記載があります。

You will find the following statement on page 6 of my column above.

■ここから導かれる一つの仮説は、江戸時代以前の"寝たきり"とは、どんなに長くても半年程度であったということです。

- One hypothesis derived from this is that "bedridden" was only for about six months before the Edo period.

■当時の介護技術で、"寝たきり"を3年とか10年のオーダーで成立させるのは、無理だったはずです。

- With the care technology available at the time, it would have been impossible to keep a person "bedridden" for three or ten years.

-----

ウクライナ支援についても、各国の「支援疲れ」は、厳然たる事実です。

As for support for Ukraine, it is a stark fact that countries are "tired of supporting" Ukraine.

期限が定められていない支援に耐えられるほど、私たちは強くないのです。

We are not strong enough to withstand support without a set deadline.

-----

結論:

Conclusion:

(1)「他人への、条件のない愛情(無償の愛)のストックには上限がある」

(1) "There is an upper limit to the stock of unconditional love (free love) for others."

(2)「そのストックは、概ね6ヶ月で尽きる」

(2) "Its stock generally runs out in six months."

(3)「被災地支援、寝たきりの人への介護が「無償」で継続できる期間は、最長で"半年"である」

(3) "The maximum period during which support for disaster-affected areas and care for bedridden people can continue "free of charge" is "six months.

この現実をベースに、私たちは、被災者支援や高齢者介護を計画しなければならないと思います。

Based on this reality, we must plan to support the affected population and care for older people.

「無償の愛」を『無期限』とする計画は、必ず破綻します ―― "必ず"です。

The plan to make "free love" "indefinite" will surely fail -- "surely."

事業仕分けに関する一考察

 

2024,江端さんの技術メモ

User
You
fastapiでjson形式のメッセージを受けとった後、処理を行い、別のjsonメッセージを返信して、終了するAPIを考えます。
この場合、別のjsonのメッセージを送信して、それの受信が返ってこないと、APIが終了しません(デッドロックしてしまう)。この問題を解決するfastapiの簡単なプログラムを作成して下さい

■fastapi側プログラム dummy2.py

from fastapi import FastAPI
from pydantic import BaseModel
import asyncio  # asyncio モジュールをインポート

app = FastAPI()

class InputMessage(BaseModel):
    value: int

class OutputMessage(BaseModel):
    response: int  # 数値を含むレスポンスメッセージ

@app.post("/process")
async def process(input_message: InputMessage):

    # 5秒間の待機
    await asyncio.sleep(5)

    # サーバー側で数値を受け取り、同じ数値を含むレスポンスメッセージを返す
    response_value = input_message.value
    output_message = OutputMessage(response=response_value)
    return output_message

起動方法は、
uvicorn dummy2:app --host 0.0.0.0 --reload

■メッセージ送信側(クライアント)プログラム dummy3.py

import httpx
import sys
import asyncio  # asyncio モジュールをインポート

async def send_message_to_api(value: int):
    url = "http://localhost:8000/process"  # FastAPIサーバーのエンドポイントURLを指定

    input_message = {"value": value}  # 送信する数値をJSONメッセージに含める

    async with httpx.AsyncClient() as client:
        response = await client.post(url, json=input_message, timeout=10.0)

    if response.status_code == 200:
        data = response.json()
        print(f"APIからのレスポンス: {data['response']}")
    else:
        print(f"エラーが発生しました。ステータスコード: {response.status_code}")

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("使用法: python send.py <数値>")
        sys.exit(1)

    try:
        value_to_send = int(sys.argv[1])  # コマンドライン引数から数値を取得
    except ValueError:
        print("数値を指定してください。")
        sys.exit(1)

    asyncio.run(send_message_to_api(value_to_send))

起動方法は、
>python dummy3.py 10

平行に3つ起動しても、正確に非同期処理してくれるようです。

昨夜から、デッドロック問題で、困っていたので、原点に戻って考え中です。

2024,江端さんの技術メモ

プロセスを強制終了しなければならないAPIを作っているのですが、「pythonからPIDをkillできない件」で困っていました。
で、以下の実験用プログラムを作成しました。

import sys
import os
import signal
def kill_process(pid):
    try:
        os.kill(pid, signal.SIGKILL)  # 指定したプロセスIDをSIGKILLシグナルで終了
        print(f"Process with PID {pid} has been killed.")
    except OSError:
        print(f"Failed to kill process with PID {pid}.")
if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python kill_process.py <PID>")
        sys.exit(1)
    try:
        pid = int(sys.argv[1])
        kill_process(pid)
    except ValueError:
        print("Invalid PID. Please enter a valid integer PID.")

で、

python3 kill_process.py 7870

を連発したのですが、全くプロセスが消えません(ps -ef | grep xxxxxなどでレビュー)

どうやら、最初に"sudo"を付けて、

sudo python3 kill_process.py 7870

で、起動してくれることが分かりました。

で、本家の問題ですが、fastapiを使ったプログラムを試してみたのですが、

sudo uvicorn test:app --host 0.0.0.0 --reload

では、エラーになります。これは環境変数を引きついでいない、とのことで、"-E"を付与することで、動くことを確認しました。

sudo -E uvicorn test:app --host 0.0.0.0 --reload

持っていかれた時間は4時間くらいかなぁ。
それでも、とりあえず動いて、次の開発に進めるので、安堵しています。

(こういう案件を夜に残すと、夜の眠りが浅くなる)。